Данная статья является вольным переводом статьи Optimizing C++/Code optimization/Faster operations. Оригинал найти можно по ссылке. Первая часть лежит здесь.
Часть 2
Префиксный или постфиксный оператор
Префиксный оператор предпочтительнее постфиксного. При работе с примитивными типами префиксные и постфиксные арифметические операции, вероятно, будут иметь одинаковую производительность. Однако с объектами, операторы постфикса могут заставить объект создать собственную копию, чтобы сохранить свое начальное состояние (которое должно быть возвращено в результате операции), а также вызвать побочный эффект операции. Рассмотрим следующий пример:
class IntegerIncreaser
{
int m_Value;
public:
/* Postfix operator. */
IntegerIncreaser operator++ (int) {
IntegerIncreaser tmp (*this);
++m_Value;
return tmp;
};
/* Prefix operator. */
IntegerIncreaser operator++ () {
++m_Value;
return *this;
};
};
Поскольку операторы постфикса обязаны возвращать неизмененную версию значения, которое увеличивается (или уменьшается) — независимо от того, используется ли результат на самом деле — скорее всего, он сделает копию. Итераторы STL (например) более эффективны при изменении с помощью префиксных операторов.
Встроенные функции
Если вы не используете параметры компилятора для оптимизации всей программы, чтобы компилятор мог встраивать любую функцию, то есть смысл перенести некоторые функции в заголовочные файлы как inline
функции, то есть объявить их встроенными.
Если верить руководству (например компилятора gcc "5.34 An Inline Function is As Fast As a Macro"), то inline
функция выполняется (так же быстро как макрос) быстрее чем обычная из-за устранения служебных вызовов, но стоит учитывать, что не все функции будут работать быстрее, а некоторые функции, объявленные как inline
способны замедлить работу всей программы.
Целочисленное деление на постоянную
Когда вы делите целое число (которое является положительным или равным нулю) на константу, преобразуйте целое число в unsigned.
Например, если s — целое число со знаком, u — целое число без знака, а C — выражение с постоянным целым числом (положительное или отрицательное), операция s / C медленнее, чем u / C, а s% C медленнее, чем u% C. Это проявляется наиболее явно, когда С — степень двойки, но, все же, при делении знак стоит учитываться.
Кстати, преобразование из signed
в unsigned
ничего не будет нам стоить, поскольку это только другая интерпретация одних и тех же битов. Следовательно, если s — целое число со знаком, которое будет использоваться в дальнейшем, как положительное или ноль, вы можете ускорить его деление, используя следующие выражения: (unsigned
) s / C и (unsigned
) s% C.
Использование нескольких массивов вместо полей структуры
Вместо обработки одного массива совокупных объектов параллельно обрабатывайте два или более массива одинаковой длины. Например, вместо следующего кода:
const int n = 10000;
struct { double a, b, c; } s[n];
for (int i = 0; i < n; ++i) {
s[i].a = s[i].b + s[i].c;
}
следующий код может быть быстрее:
const int n = 10000;
double a[n], b[n], c[n];
for (int i = 0; i < n; ++i) {
a[i] = b[i] + c[i];
}
Используя эту перегруппировку, «a», «b» и «c» могут обрабатываться командами обработки массива, которые значительно быстрее, чем скалярные инструкции. Эта оптимизация может иметь нулевые или неблагоприятные результаты для некоторых архитектур.
Еще лучше перемежать массивы:
const int n = 10000;
double interleaved[n * 3];
for (int i = 0; i < n; ++i) {
const size_t idx = i * 3;
interleaved[idx] = interleaved[idx + 1] + interleaved[idx + 2];
}
PS: Учтите, что каждый случай нужно тестировать, а не оптимизировать преждевременно.
Комментарии (20)
Akon32
06.10.2017 13:30+2следующий код может быть быстрее:
…
Еще лучше перемежать массивы:
...Чем лучше? Получаем тот же вариант(1), что и со структурой.
oYASo
06.10.2017 13:39+2Увы, все это уже не сильно актуально для современного C++. В современном мире плюсов нужно больше думать о своих структурах данных и алгоритмах, и в меньшей степени — о микрооптимизациях, о чем, в общем-то, говорят и сами разработчики языка.
Akon32
06.10.2017 14:26"Современный мир" разный. После того, как свои структуры данных и алгоритмы идеально реализованы и всё равно тормозят, можно дойти до микрооптимизаций.
yleo
06.10.2017 14:24+4Не стану минусовать статью, так как автор перевода точно не виноват.
Но, думаю, стоит пояснить, что тут не так.
Самое главное в оптимизации — это, конечно, результат. Примерно по секундомеру или по количеству необходимой памяти. Беда в том, что "просто оптимизация", особенно описываемые механические приёмы/трюки, быстро приводят к попыткам оптимизировать "всё что плохо лежит".
Выходит даже не преждевременная оптимизация, а это code style — часто очень трудно читаемый.
В таком code style можно написать пару тысяч строк кода, потом будет просто трудно и неудобно. Иногда это оправдано и даже дано. Например, разработчики привыкшие делать именно так, могут автоматически сделать супер-быстрым почти любой код (по любому ТЗ). Но не дай бог, чтобы такого кода было много — его реально сложно поддерживать, дорабатывать, верифицировать, высока вероятность внесения дефектов и т.д. Тем не менее, это не основное "зло" в статье, это побочный эффект.
Главное в оптимизации — правильный вкус: чувство меры и ощущения стиля одновременно. Это трудно достижимый навык, некий дзен, мастерство в искусстве. Уже было много раз составлялись всяческие своды правил и/или наборы механических приёмов — результаты плачевны (хотя точно больше нуля). Более того, получить чувство меры невозможно без набивания достойных шишек.
Собственно, основной принцип оптимизации давно озвучил Буонарроти = уберите лишнее. Просто избавьте машину от лишней работы.
А вот тут внезапно выясняется, что для понимания "лишнего" нужно хорошо представлять как работает машина: всяческие кэш-линии, MESI, зависимости по данным в конвейере u-ops, что делает оптимизирующий компилятор, зачем те или иные конструкции языка, когда/зачем/как случается векторизация, и ещё очень-очень много. И убирать нечно "лишнее" нужно там, где оно действительно лишнее, но не поворачивать реки.
Так вот, основной минус этой статьи в том, что вместо понимания "как на самом деле" по-большей части предлагается культ Карго (делайте так и будет вам счастье).
Serge78rus
06.10.2017 19:15Префиксный инкремент вместо постфиксного, там где не важно возвращаемое значение, уж точно не ухудшит читаемость кода.
Wild__Recluse
06.10.2017 15:07+2а некоторые функции, объявленные как inline способны pfvtlkbnm работу всей программы
Кажется, вы имели ввиду «замедлить»?
DareDen
06.10.2017 18:11Высказался по первой части, выскажусь и по второй.
Опять привет из далекого прошлого — в статье ссылка на документацию gcc-3.3.6.
По поводу массивов, структур и перемежения листинг от Compile Explorer'a — разницы нет :). Вообще.
В 2017м нет смысла соревноваться с компилятором, он знает куда больше трюков, чем вы, смиритесь :).mobi
06.10.2017 18:49Разница есть если компилировать с ключами
-O3 -mavx2
. Или интелловским компилятором (icc).DareDen
06.10.2017 19:00Хорошо, соглашусь. Разница есть, но только в случае разных массивов, про AVX векторизацию я не подумал, но объявленный самым эффективным перемежающийся вариант ничем не лучше структуры (что очевидно, структура размещена в памяти ровно так же, как перемежающийся массив).
Einherjar
Эх, будто в 2008 год попал. Бумажные книги по разработке 3d игр, 0x5F3759DF, вычисление тригонометрических функций через ряд тейлора…