Блог компании Mail.Ru Group
C++
Программирование
Совершенный код
Комментарии 87
+3
Старайтесь использовать ссылки, а не указатели. Ссылки не требуют проверок. Ссылка непосредственно указывает на объект, а указатель содержит адрес, который нужно прочитать

А можно поподробнее? А то я всю жизнь считал, что Foo* foo1 и Foo& foo2 это одно и то же, за исключением того, что foo2 не надо явно разыменовывать, а адрес оно будет в любом случае читать. Ну и джентльменского соглашения о том, что в foo2 мне не собираются подсовывать nullptr. (хотя и могут)
+8
Не думаю, что в программировании применимы джентльменские соглашения. Если указатель может указывать на null, рано или поздно он будет туда указывать. Если деструктор может сгенерировать исключение, рано или поздно это произойдет. Если обработчик сигнала не AS-Safe, рано или поздно это приведет к dead-lock. Поэтому указатели используют там, где нужен именно указатель. И обкладывают проверками.
0
а вот эту часть проясните:

Ссылка непосредственно указывает на объект, а указатель содержит адрес, который нужно прочитать

msvc, gcc и шланг сгенерят идентичный код с чтением указателя. или я пьян?
+1
Если постараться, в C++, как и в других низкоуровневых языках, можно сделать все, что угодно. Однако ссылку даже нельзя проверить на валидность, т.к. это уже UB.
-1
Ну что стараться =) в большой компании это неизбежно. Если вы работаете над большим проектом и кроме вас в его кишках ковыряется 50+ человек, то тут и стараться не надо. Оно появляется постоянно… люди глупы по своей сути и склонны ошибаться. Да с сылкой — самые сложные баги, хрен поймешь что произошло.
0
Указатель тоже в общем случае нельзя проверить на валидность. Можно сравнить его с 0, но кроме 0, есть и другие невалидные значения указателя, которые проверить нельзя.
+1
Кто вам сказал, что 0 — невалидное значение указателя (невалидный адрес)? Не забывайте, мы не про С говорим, а про С++.
0
Какая разница, что вы думаете насчет джентльменских соглашений? Конкретно то, о чем пишет atd — это undefined behavior по стандарту языка.
0
Правильно абсолютно считаете. Автор плохо знает С++ и архитектуру процессоров. Технически это абсолютно одно и то же.
Разница в том, что указатель может быть нулл, а ссылка не может. Ну то есть можно сознательно прострелить себе ногу и превратить нуллптр в ссылку, но это UB.
В контексте оптимизации все верно, ссылку, в отличие от указателя не нужно каждый раз проверять.
Только это все из серии "вы можете хранить целые числа в типе double, но зачем"? Разные инструменты для разных задач.
+2
Автор достаточно хорошо знает, просто невнимательно прочитал комментарий.
+2
Если бы автор знал С++ достаточно хорошо, то не написал бы: «Ссылка непосредственно указывает на объект, а указатель содержит адрес, который нужно прочитать».
0
ссылку, в отличие от указателя не нужно каждый раз проверять

Указатель тоже не всегда нужно каждый раз проверять. Более того, сравнение с null — это проверка только на один из видов невалидного указателя. Бывают еще ненулевые, но тем не менее невалидные указатели, которые вы никакими проверками не обнаружите.
+2
В неоптимизированном коде ссылки и указатели будут работать одинаково. Однако при оптимизации кода использование ссылок даёт компилятору больше пространства для оптимизаций: для того, чтобы провести некоторую оптимизацию, компилятору нужно сначала доказать корректность, т.е. то, что при этой оптимизации семантика сохраняется. Для ссылок такие утверждения доказывать легче, т.к. они не могут быть переназначены, в отличие от указателей.
0
Foo * const foo1 (обратите внимание на const) и Foo & foo2 — вот что одно и то же на бытовом уровне понимания.
Но фактически ссылки лучше рассматривать не как "указатели, но только без звёздочек и стрелочек", а как синонимы, т.е. ссылка — это просто альтернативное имя для переменной, так удобнее думать. Потому что компилятор в большинстве случаев соптимизирует все ссылки, т.е. они скорее всего (если вы особенно не постараетесь) не будут ничего стоить дополнительно.
0
Поэтому я отметил: "Этот совет почти потерял актуальность" :-)

Есть move semantics, но пока не у всех классов есть конструктор перемещения, чтобы компилятор мог это задействовать.
+4
Как это бывает, новая фича не решает всех проблем. Move semantics не заменяет RVO полностью. В Effective Modern C++ есть об этом отрывок:

Item 25
In other words, they figure that given a function returning a local variable by value, such as this,

Widget makeWidget()  // "Copying" version of makeWidget
{
Widget w;            // local variable
...                  // configure w
return w;            // "copy" w into return value
}

they can “optimize” it by turning the “copy” into a move:

Widget makeWidget()  // Moving version of makeWidget
{
Widget w;
...
return std::move(w); // move w into return value
}                    // (don't do this!)

My liberal use of quotation marks should tip you off that this line of reasoning is flawed. But why is it flawed?
It’s flawed, because the Standardization Committee is way ahead of such program‐mers when it comes to this kind of optimization. It was recognized long ago that the “copying” version of makeWidget can avoid the need to copy the local variable w by constructing it in the memory alloted for the function’s return value. This is known as the return value optimization (RVO), and it’s been expressly blessed by the C++ Standard for as long as there’s been one.

Wording such a blessing is finicky business, because you want to permit such copy elision only in places where it won’t affect the observable behavior of the software. Paraphrasing the legalistic (arguably toxic) prose of the Standard, this particular blessing says that compilers may elide the copying (or moving) of a local object3 in a function that returns by value if (1) the type of the local object is the same as that returned by the function and (2) the local object is what’s being returned. With that in mind, look again at the “copying” version of makeWidget:

Widget makeWidget() // "Copying" version of makeWidget
{
Widget w;
...
return w; // "copy" w into return value
}

3 Eligible local objects include most local variables (such as w inside makeWidget) as well as temporary objects created as part of a return statement. Function parameters don’t qualify. Some people draw a distinction between application of the RVO to named and unnamed (i.e., temporary) local objects, limiting the term RVO to unnamed objects and calling its application to named objects the named return value optimization (NRVO).

Both conditions are fulfilled here, and you can trust me when I tell you that for this code, every decent C++ compiler will employ the RVO to avoid copying w. That means that the “copying” version of makeWidget doesn’t, in fact, copy anything. The moving version of makeWidget does just what its name says it does (assuming Widget offers a move constructor): it moves the contents of w into makeWidget’s return value location. But why don’t compilers use the RVO to eliminate the move, again constructing w in the memory alloted for the function’s return value? The answer is simple: they can’t. Condition (2) stipulates that the RVO may be performed only if what’s being returned is a local object, but that’s not what the moving version of makeWidget is doing. Look again at its return statement:

return std::move(w);

What’s being returned here isn’t the local object w, it’s a reference to w—the result of std::move(w). Returning a reference to a local object doesn’t satisfy the conditions required for the RVO, so compilers must move w into the function’s return value location. Developers trying to help their compilers optimize by applying std::move to a local variable that’s being returned are actually limiting the optimization options available to their compilers!

But the RVO is an optimization. Compilers aren’t required to elide copy and move operations, even when they’re permitted to. Maybe you’re paranoid, and you worry that your compilers will punish you with copy operations, just because they can. Or perhaps you’re insightful enough to recognize that there are cases where the RVO is difficult for compilers to implement, e.g., when different control paths in a function return different local variables. (Compilers would have to generate code to construct the appropriate local variable in the memory allotted for the function’s return value, but how could compilers determine which local variable would be appropriate?) If so, you might be willing to pay the price of a move as insurance against the cost of a copy. That is, you might still think it’s reasonable to apply std::move to a local object you’re returning, simply because you’d rest easy knowing you’d never pay for a copy.

In that case, applying std::move to a local object would still be a bad idea. The part of the Standard blessing the RVO goes on to say that if the conditions for the RVO are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue. In effect, the Standard requires that when the RVO is permitted, either copy elision takes place or std::move is implicitly applied to local objects being returned. So in the “copying” version of makeWidget,

Widget makeWidget() // as before
{
Widget w;
...
return w;
}

compilers must either elide the copying of w or they must treat the function as if it were written like this:

Widget makeWidget()
{
Widget w;
...
return std::move(w); // treat w as rvalue, because
                     // no copy elision was performed
}

The situation is similar for by-value function parameters. They’re not eligible for copy elision with respect to their function’s return value, but compilers must treat them as rvalues if they’re returned. As a result, if your source code looks like this,

Widget makeWidget(Widget w) // by-value parameter of same
{                           // type as function's return
...
return w;
}

compilers must treat it as if it had been written this way:

Widget makeWidget(Widget w)
{
...
return std::move(w); // treat w as rvalue
}

This means that if you use std::move on a local object being returned from a function that’s returning by value, you can’t help your compilers (they have to treat the local object as an rvalue if they don’t perform copy elision), but you can certainly hinder them (by precluding the RVO). There are situations where applying std::move to a local variable can be a reasonable thing to do (i.e., when you’re passing it to a function and you know you won’t be using the variable any longer), but as part of a return statement that would otherwise qualify for the RVO or that returns a by-value parameter isn’t among them.

0
Краткое содержание item25:
RVO эффективнее move семантики, так как в первом случае переменная аллоцируется уже нужном месте и не копируется, а во втором случае отрабатывает move constructor.
+2
п.2.1. Разворачивание циклов.

Не стоит предлагать неверную замену кода в статье, даже если вы написали "что-то вроде".
Люди скопируют ошибочный код, результат не предсказуем.
+5
Основные советы Вы действительно перечислили и почти всегда их хватает для обычной разработки типичных IT-задач. Есть ещё книга Скотта Майерса "Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ", в которой эти вещи, помимо прочего, тоже раскрыты.

Однако последние два совета о том, что нужно предпочитать готовые решения и стремиться к простоте кода следовало бы дать с пояснением, что это не всегда и не везде так. В соседней статье я дал комментарий (довольно длинный), в котором перечислил ряд причин, когда необходимо изобретать велосипед, более того, в этих случаях вредно предпочитать готовые решения.

А совет о простоте кода не всегда выполним. Бывают задачи, в которых сделать код простым просто нельзя. Он в принципе сложен, потому что сложен сам алгоритм и его понимание требует хорошей математической подготовки. Например, алгоритм Шёнхаге — Штрассена для перемножения длинных чисел. В исходниках GMP можно посмотреть и убедиться, что сделать его проще можно лишь в ущерб производительности, что для данной библиотеки недопустимо. Кто знает математику, лежащую в основе алгоритма, тот разберётся в этом коде, а кто не знает, один фиг, просто или сложно, всё китайская грамота.

Короче говоря, нужно учитывать область работы программиста, когда даются такие советы. В остальном согласен.
+1
Если s — это пустая строка (""), то s[0] = 0 — тут проблем нет

А вот если s это nullptr, то s[0] приведет к неверному чтению из памяти, поэтому надо проверять в самом начале, что s не 0 (NULL, nullptr)
0
Вы добавили '*s', а надо просто 's', т.к. s в данном случае равно s[0]
-10
Вы поклонник Григория Остера?
Какой-то сборник вредных советов и заблуждений.
-2
+1. После п.1 желание читать дальше пропало, т. к. с оптимизациями компилятора автор незнаком.
+4
Ну если уж набросили, так дайте конкретику — в чем вред, где заблуждение?
А так получается ляпнул и пошел дальше. А что сказать хотел — неизвестно.
+20
Не используйте vector там, где можно было бы обойтись list или deque

Не согласен — используйте vector, пока профайлер не скажет обратного. "Discontinuous data structures are the root of all (performance) evil": https://youtu.be/fHNmRkzxHWs?t=35m

Вкратце — доступ к кэшам процессора на 1-2 порядка быстрее, чем доступ к оперативке, данные из оперативки попадают в кэши непрерывными кусками, а данные в std::list раскиданы по памяти => любые чтения из list будут крайне затратными.

Я бы использовал list, только если количество вставок/удалений в середину на порядки больше, чем количество чтений из него, или если программируешь для нестандартного железа.
0
А если уж вектор будет постоянно расти, то там шаг выделения памяти при реалокации достаточно большой, чтобы в конце концов свести их на нет. Правда, говорят, раньше его делали в районе 1,5.
-1
Насчет компактного размещения ключей хэш-таблицы в кэше повеселили)
+5
Используйте префиксную форму инкремента и декремента

Однажды в книге Game Engine Architecture я наткнулся на следующий абзац:

Notice in the above example that we are using C++’s postincrement operator,
p++, rather than the preincrement operator, ++p. This is a subtle but sometimes
important optimization. The preincrement operator increments the contents
of the variable before its (now modified) value is used in the expression.
The postincrement operator increments the contents of the variable after it has
been used. This means that writing ++p introduces a data dependency into your
code—the CPU must wait for the increment operation to be completed before
its value can be used in the expression. On a deeply pipelined CPU, this introduces
a stall. On the other hand, with p++ there is no data dependency. The
value of the variable can be used immediately, and the increment operation
can happen later or in parallel with its use. Either way, no stall is introduced
into the pipeline.
Of course, within the “update” expression of a for loop (for(init_expr;
test_expr; update_expr) {… }), there should be no difference between
pre- and postincrement. This is because any good compiler will recognize that
the value of the variable isn’t used in update_expr. But in cases where the
value is used, postincrement is superior because it doesn’t introduce a stall
in the CPU’s pipeline. Therefore, it’s good to get in the habit of always using
postincrement, unless you absolutely need the semantics of preincrement.

Т.е. совет — делать с точностью да наоборот. Не указано, к каким процессорам это применимо, поэтому исхожу из предположения, что к большинству современных, в т.ч. на PC и игровых консолях. Что на этот счёт скажете?
0
Я бы сказал, что не надо у своих классов перегружать операторы инкремента. Я вообще не знаю, как это оправдать можно.
А для примитивных типов вроде бы безразлично.
0
Речь о примитивных типах. В контексте перегруженных операторов вообще трудно рассуждать о подобных оптимизациях.
0
Резонно, как-то я про них забыл. Давненько не приходилось свой писать.
Впрочем, если использовать range-based for, то вызываться будет только префиксный инкремент.
0
Скажу, что мой совет относился к стандартным итераторам, в которых хранятся "большие" объекты. В случае чего-нибудь вроде int очень даже допустимо, что все будет с точностью наоборот, правильное замечание.
0
Этот абзац относится только типам данных, помещающимся в регистр. Если p не влезает в регистр, то не получится распараллелить чтение и запись, т.к. распараллеливание идёт на уровне инструкций, а они работают с регистрами.
0
Мои измерения не подтверждали этот совет (во всяком случае с современными компиляторами и процессорами).
Что на самом деле легко понять — зависимость есть в обоих случаях, а разница в том, где расположена инструкция инкремента (и то если компилятор не смог сам справиться).

Преинкремент:

  1. Инкремент
  2. Использование нового значения
  3. Остальное тело цикла

Постинкремент

  1. Использование значения
  2. Инкремент
  3. Тело цикла

Или, если рассмотреть несколько итераций:

123123123 против 213213213

Современные процессоры не только конвейиризованные, но и со внеочередным исполнением, и он сам переставляет команды так как ему хочется, и вполне может стравиться в перестановкой 1 и 3 во втором случае, что сделает цикл эквивалентным первому.
0
Согласен, std::string плохой пример, т.к. он имеет конструктор перемещения. Изменил на абстрактную структуру.
+6
1.6. Не создавайте временные объекты — 2

C++ — это не C, где объявление переменных должно располагаться в самом начале.

Автор видимо не в курсе, что есть C99, C11.
+1
Про Cache line ping-pong — тут скорее про false sharing, ибо если два потока обращаются и к a, и к b, и к c, то никакое выравнивание не спасёт, т.к. будут гоняться три строки кэша вместо одной.
+3
Совсем правильный пример конкатенации должен быть таким:
auto s = concat(s1,s2,s3);
, где concat — ленивый диапазон отсюда (https://github.com/ericniebler/range-v3), ну или из std, когда будет.
В этом случае вообще не произойдёт выделения памяти. Понятно, что если дальше нужен быстрый произвольный доступ, то всё равно лучше создать именно строку через range::copy(concat(...), s). Но если потом вы будете читать пару символов или только последовательно, то лучше оставить ленивый диапазон.
+5
2.6 — не правда.
http://en.cppreference.com/w/cpp/language/copy_elision
И это не единственный пример дезинформации.
Вообще такое ощущение, что автор пишет о каких то древних компиляторах, не умеющих в оптимизацию. Они давно достаточно умны, что бы почти обо всех пунктах заботиться не приходилось.
+2
Что именно неправда? Что NRVO не всегда может быть задействована? Или что не у всех классов есть конструктор перемещения? Что в примере не может быть задействовано RVO? Так и по ссылке тоже самое:

std::vector<Noisy> f()
{
    std::vector<Noisy> v = std::vector<Noisy>(3); // copy elision from temporary to v
    return v; // NRVO from v to the returned nameless temporary
}             // or the move constructor is called if optimizations are disabled

Или про "совет почти потерял актуальность"?

Вы уж поконкретней, пожалуйста :-)
+1
В примере "плохой код" в этом пункте NRVO будет задействована. Поэтому считать этот код не оптимальным и говорить что надо писать только так нельзя.
0
А кто говорит про NRVO? Я написал "// Компилятор не может использовать RVO". Не NRVO, а RVO. "Плохой код" — это образное выражение относительно RVO, разумеется в нем нет ничего плохого.
+2
1.10 Вредный совет.
vector при последовательном добавлении элементов всегда будет оптимальнее. Да, тогда когда он упирается в границы выделенной памяти, придется выделить новый блок, но все реализации выделяют память с запасом — в 1,5 / 2 раза больше от текущей. В результате, копирование и аллокация блока в куче — не такое уж частое явление, тогда как std::list делает аллокацию под каждый новый элемент. И это уж если не говорить о том, что у вектора самый быстрый последовательный и случайный доступ к элементам. В результате я бы сказал по другому — всегда используйте vector, если нет веских причин использовать list или deque.
0
Согласен, что совет спорный. Сильно зависит от количество вставок и того, куда вставляется. Но я слишком часто на практике сталкивался с тем, что приходилось переделывать с vector на deque или list. Поэтому призываю думать, а не бездумно писать vector в надежде, что кто-нибудь потом регулярно будет прогонять код профайлером под изменившиеся условия и исправит, когда vector станет работать плохо.
0
1.8. Во всех актуальных реализациях std::string есть small string optimization. Для маленьких, в том числе строк нулевого размера, аллокации в куче не будет.
+1
Конечно выделения памяти не будет, строка вообще пустая, поэтому я написал "Он будет пытаться". Это означает, отработает совсем другая ветка кода.
0
Если мне не изменяет память, то libstdc++ который шёл в комплекте GCC до версии 5.x было не SSO, а COW. Да собственно, Саттер подтверждает. А это покрывает достаточно популярные 4.8 и 4.9.
0
В пункте про RVO логично было бы упомянуть про NRVO и его ограничениях.
+4
1.10. Не используйте vector там, где можно было бы обойтись list или deque

Для современных декстопов не очень актуальный совет, за счет последовательного расположения в памяти вектор, даже с учетом изменений может быть быстре листа: Тыц
0
Повторюсь, что совет появился как раз из-за исправления с vector на что-либо другое. Если количество элементов мало, то обычно нет разницы, какой контейнер. Если же оно большое, например 1 000 000, может быть deque будет все же эффективней?

И надо думать не только о том, какое количество вставок сейчас. А еще и на много лет вперед, когда проект будут поддерживать совсем другие люди.
-1
std::vector vec;
for(size_t i = 0; i < vec.size(); ++i)

Потому что в данном случае он дешев. Это будет эквивалентно следующему коду:

size_t size = vec.size();
for(size_t i = 0; i < size; ++i)

Да ладно, с чего вдруг эквивалентно, когда есть семантическая разница?

/offtopic с оформелнием кода в цитатах какой-то ад
+2
Дополнительные условия внутри цикла или же вычисления, влияющие на счетчик цикла, приведут к невозможности развернуть цикл.

Разворачиванием циклов компилятор занимается самостоятельно. Ему следует помочь только тем, чтобы это можно было сделать. Также небольшие циклы желательно объединять в один. Но при наличии условий или большого тела цикла, наоборот, бывает лучше разбить на несколько циклов, чтобы хотя бы один из них был развернут компилятором.

Я просто оставлю это здесь: http://llvm.org/docs/Vectorizers.html

The Loop Vectorizer is able to “flatten” the IF statement in the code and generate a single stream of instructions. The Loop Vectorizer supports any control flow in the innermost loop. The innermost loop may contain complex nesting of IFs, ELSEs and even GOTOs.

int foo(int *A, int *B, int n) {
  unsigned sum = 0;
  for (int i = 0; i < n; ++i)
    if (A[i] > B[i])
      sum += A[i] + 5;
  return sum;
}

В статье очень много сомнительных мест, которые просто напросто дезинформируют читателя.
0
Интересно а в рамках какого проекта вы столкнулись с рассмотрением кода LinuxDC++/StrongDC++ ?
0
Временные объекты создают, к примеру, вот таким кодом:

std::string s1, s2, s3;

std::string s = s1 + s2 + s3;

Это действительно проблема и в последних стандартах языка? Операция — то очень часто используемая. В языках, где строковый тип встроен, вполне может быть оптимизация такого случая вида "слей-ка мне в одну N строк, вот они".
0
Стандарт об этом ничего не говорит и не должен, но компиляторы вольны оптимизировать это как считают нужным (std::string не "встроенный", но тем не менее для классов стандартной библиотеки компиляторы зачастую применяют особые оптимизации)
0
STLPort склеивает строки лениво.
Конкатенация строк возвращает некоторый объект, который тоже умеет складываться со строкой.
И только в момент вызова оператора operator string() происходит выделение буфера и склейка.
-1
Вот за такие посты обычно ставлю плюсы везде, где и кому это возможно.
+7
Обложили бедного автора по-всякому, потому что в С++ какой тезис не двинь, как окажется, что бывают случаи\стандарты\компиляторы\железо\процессоры\память\алгоритмы, где это не выполняется :)
+2
А, ещё момент. Перед тем, как говорить, что какой-то вариант быстрее и меньше инструкций и т.п., заходите на http://gcc.godbolt.org, справа там вписывайте -O3 в параметры и сравнивайте. Очень много удивлений будет.
+1
Например, проверьте там свой пример 2.4 про switch или if :-))
0
  1. Проверил, самый свежий gcc (5.3.0) все еще генерирует вот это:
    func(int):
        cmpl    $1, %edi
        je      .L3
        cmpl    $2, %edi
        je      .L4
        cmpl    $3, %edi
        je      .L5
        cmpl    $4, %edi
        je      .L6
        cmpl    $5, %edi
        movl    $100, %edx
        movl    $50, %eax
        cmovne  %edx, %eax
        ret
    .L6:
        movl    $40, %eax
        ret
    .L3:
        movl    $10, %eax
        ret
    .L4:
        movl    $20, %eax
        ret
    .L5:
        movl    $30, %eax
        ret

Все еще никакой оптимизации со стороны компилятора.
  1. Конечно информация имеет свойство устаревать, это факт. Я в самом начале указал, что все это написано давно (2006-2008, если мне не изменяет память). Так что же, теперь ничего не писать?
+1
Кстати да, GCC в подобных случаях в чём-то не может до конца разобраться. А вот clang генерит полностью идентичный код для if и switch из примера.
В этом посте от 2010 разбираются некоторые мифы об оптимизациях: http://ridiculousfish.com/blog/posts/will-it-optimize.html — там автор подобную картину наблюдает (последний пример).
0
забудьте про gcc, только clang
gcc оставляем только как референсный компилятор в случаях, когда возникают сомнения в коде clang (но и то в 100% случаев clang прав)
+1
Нельзя забыть про то, что настроено и работает годами. Clang не идеальный компилятор и далеко не все умеет оптимизировать также хорошо.
0
то, что не умеет gcc, мы видим в примере выше (элементарщина же)
что не умеет clang? ну я к тому, что утверждая что-то общее, хотелось бы увидеть хотя бы примеры
0
Тем не менее, на реальных программах gcc все еще генерит более быстрый код чем clang
0
Они оба генерят более быстрый код. Просто на разных реальных примерах. Я как то сравнивал. Нельзя сказать какой из них быстрее. В каких то случаях Clang быстрее, в каких-то gcc.
У Clang ввод подсвечен — удобнее ошибки компиляции высматривать. Но начиная с версии 5.0 gcc тоже стал вывод раскрашивать. Так что теперь они вообще оба хороши.
Только полноправные пользователи могут оставлять комментарии. , пожалуйста.