Данная статья является вольным переводом статьи 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)


  1. Einherjar
    06.10.2017 13:13
    +2

    Эх, будто в 2008 год попал. Бумажные книги по разработке 3d игр, 0x5F3759DF, вычисление тригонометрических функций через ряд тейлора…


  1. Akon32
    06.10.2017 13:30
    +2

    следующий код может быть быстрее:

    Еще лучше перемежать массивы:
    ...

    Чем лучше? Получаем тот же вариант(1), что и со структурой.


    1. Fil
      06.10.2017 13:48

      Думаю, что компилятору так проще задействовать SSE. Но такой совет все равно скорее вредный.


      1. Akon32
        06.10.2017 14:01

        Мне кажется, что примеры 1 и 3 скомпилируются в идентичный код.


      1. kosmos89
        06.10.2017 14:10

        Нет уж, оба варианта не будут векторизованы.


        1. Fil
          06.10.2017 14:41

          Почему?

                  movapd  xmm0, XMMWORD PTR [rsp+159880+rax]
                  addpd   xmm0, XMMWORD PTR [rsp+79880+rax]
                  add     rax, 16
                  movaps  XMMWORD PTR [rsp-136+rax], xmm0
          

          vs
                  movsd   xmm0, QWORD PTR [rdx+8]
                  add     rdx, 24
                  addsd   xmm0, QWORD PTR [rdx-8]
                  movsd   QWORD PTR [rdx-24], xmm0
          


          1. yleo
            06.10.2017 14:51

            Потому-что «гладиолус». Гляньте код от icc.


          1. kosmos89
            06.10.2017 15:42

            Речь про первый и третий. Со вторым и так все понятно.


            1. Fil
              06.10.2017 15:51
              +1

              А, ну да, не заметил, извиняюсь. Третий вариант вообще ужасен — значительное усложнение кода при той же производительности.


  1. oYASo
    06.10.2017 13:39
    +2

    Увы, все это уже не сильно актуально для современного C++. В современном мире плюсов нужно больше думать о своих структурах данных и алгоритмах, и в меньшей степени — о микрооптимизациях, о чем, в общем-то, говорят и сами разработчики языка.


    1. Akon32
      06.10.2017 14:26

      "Современный мир" разный. После того, как свои структуры данных и алгоритмы идеально реализованы и всё равно тормозят, можно дойти до микрооптимизаций.


  1. yleo
    06.10.2017 14:24
    +4

    Не стану минусовать статью, так как автор перевода точно не виноват.
    Но, думаю, стоит пояснить, что тут не так.


    Самое главное в оптимизации — это, конечно, результат. Примерно по секундомеру или по количеству необходимой памяти. Беда в том, что "просто оптимизация", особенно описываемые механические приёмы/трюки, быстро приводят к попыткам оптимизировать "всё что плохо лежит".


    Выходит даже не преждевременная оптимизация, а это code style — часто очень трудно читаемый.
    В таком code style можно написать пару тысяч строк кода, потом будет просто трудно и неудобно. Иногда это оправдано и даже дано. Например, разработчики привыкшие делать именно так, могут автоматически сделать супер-быстрым почти любой код (по любому ТЗ). Но не дай бог, чтобы такого кода было много — его реально сложно поддерживать, дорабатывать, верифицировать, высока вероятность внесения дефектов и т.д. Тем не менее, это не основное "зло" в статье, это побочный эффект.


    Главное в оптимизации — правильный вкус: чувство меры и ощущения стиля одновременно. Это трудно достижимый навык, некий дзен, мастерство в искусстве. Уже было много раз составлялись всяческие своды правил и/или наборы механических приёмов — результаты плачевны (хотя точно больше нуля). Более того, получить чувство меры невозможно без набивания достойных шишек.


    Собственно, основной принцип оптимизации давно озвучил Буонарроти = уберите лишнее. Просто избавьте машину от лишней работы.


    А вот тут внезапно выясняется, что для понимания "лишнего" нужно хорошо представлять как работает машина: всяческие кэш-линии, MESI, зависимости по данным в конвейере u-ops, что делает оптимизирующий компилятор, зачем те или иные конструкции языка, когда/зачем/как случается векторизация, и ещё очень-очень много. И убирать нечно "лишнее" нужно там, где оно действительно лишнее, но не поворачивать реки.


    Так вот, основной минус этой статьи в том, что вместо понимания "как на самом деле" по-большей части предлагается культ Карго (делайте так и будет вам счастье).


    1. Serge78rus
      06.10.2017 19:15

      Префиксный инкремент вместо постфиксного, там где не важно возвращаемое значение, уж точно не ухудшит читаемость кода.


      1. yleo
        06.10.2017 19:26

        Это — да, лучше в привычку.


  1. Wild__Recluse
    06.10.2017 15:07
    +2

    а некоторые функции, объявленные как inline способны pfvtlkbnm работу всей программы


    Кажется, вы имели ввиду «замедлить»?


    1. genge Автор
      06.10.2017 15:07

      да, спасибо


  1. DareDen
    06.10.2017 18:11

    Высказался по первой части, выскажусь и по второй.
    Опять привет из далекого прошлого — в статье ссылка на документацию gcc-3.3.6.
    По поводу массивов, структур и перемежения листинг от Compile Explorer'a — разницы нет :). Вообще.
    В 2017м нет смысла соревноваться с компилятором, он знает куда больше трюков, чем вы, смиритесь :).


    1. mobi
      06.10.2017 18:49

      Разница есть если компилировать с ключами -O3 -mavx2. Или интелловским компилятором (icc).


      1. DareDen
        06.10.2017 19:00

        Хорошо, соглашусь. Разница есть, но только в случае разных массивов, про AVX векторизацию я не подумал, но объявленный самым эффективным перемежающийся вариант ничем не лучше структуры (что очевидно, структура размещена в памяти ровно так же, как перемежающийся массив).


      1. Fil
        06.10.2017 19:01

        "-O2 -ftree-vectorize" тоже работает, но не так агрессивно, конечно