Оптимизация циклов C с условными обозначениями на переменную Looping

Извините, если это задано в архивах. Я нашел некоторые подобные вопросы, но ни один из них не казался именно тем, что я хотел.

Дистиллированная версия проблемы, над которой я работаю, выглядит следующим образом. У меня есть серия вычислений для выполнения, которые будут хранить значения в 4 (очень больших) массивах: A, B, C и D. Эти вычисления взаимозависимы, например, для вычисления b [i] может потребоваться использование [i-1]. Я способен выразить все в одном цикле, но это приводит к крайним случаям, когда для определенных значений я необходимо выполнить только некоторые вычисления. Например:

for(i=0;i<end;i++)
{
    if(i == 0)
        //calculate A[i+1] and B[i+1]
    else if (i == end-1)
        //calculate C[i-1] and D[i-1]
    else
        //calculate A[i+1], B[i+1], C[i-1], D[i-1]
}

Для вопросов производительности я хотел бы избежать наличия условий в моем цикле. Оценка условного значения была бы дешевой по сравнению с расчетами, но, возможно, не была бы незначительной. Мой вопрос заключается в том, что компилятор может надежно расширить его до

//calculate A[1] and B[1]
for(i=1;i<end-1;i++)
{
    //calculate A[i+1], B[i+1], C[i-1], D[i-1]
}
//calculate C[end-2] and D[end-2]

Я собираюсь из архивов, что компилятор разбивает мой цикл, если условные выражения были постоянными, но здесь они зависят от i, что в принципе может быть изменено некоторыми моими вычислениями. Будет ли он обнаруживать, что я не изменяю переменную итерации и, таким образом, разделяю ее разумным образом?

Дополнительная информация, если вы решите ответить на вопрос, предложив лучший способ сделать что-то:

Первоначально код был написан с 4 циклами, чтобы вычислять элементы для каждого из массивов. Это был самый интуитивный способ написания кода, но он был неэффективен. Поскольку вычисление элементов в одном массиве зависело от элементов в других массивах, это означало, что мне приходилось читать во всех 4 массивах из памяти во время каждой из 4 циклов. Поскольку эти массивы не вписываются в кеш, это не оптимально, и мне нужен код, который будет циклически проходить через мои массивы только один раз.

Я также знаю, что я могу разбить свою петлю вручную, и именно так дела идут в данный момент. Однако эти вычисления включают нетривиальные формулы (и я не могу позволить себе получить производительность при вызове функции во время каждой итерации этого цикла), поэтому разрывание кода вызвало дублирование кода, которое не только очень трудно читать, но и почти недостижимо следующее время мои формулы получить tweaked (что они будут...)

Спасибо заранее!

Ответ 1

Чтобы ответить на ваш вопрос в более широком смысле: когда оптимизация важна, профилировщик - ваш друг. Разработчики, как известно, плохо разбираются в том, где в нашем коде процессор проводит большую часть своего времени. Профилист покажет вам, где именно "дорогие" операции, поэтому вы можете сосредоточиться на фиксации областей, которые дадут вам наиболее значительные улучшения.

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

Другие вещи, о которых нужно подумать:

  • В качестве эксперимента переиндексируйте ваши вычисления, чтобы они максимально точно работали на i, а не i-1 или i+1. Так, например, используйте A[i], B[i], C[i-2] и D[i-2]. Я был бы удивлен, если бы это значительно улучшилось с помощью оптимизирующего компилятора, но вы никогда не знаете....

  • Прекомпопуйте все, что можете.

  • Попробуйте разбить свои вычисления на более мелкие компоненты, которые являются либо постоянными, либо обычными, как предположил Джеймс Гринхалх, поэтому они могут быть повторно использованы.

  • Можете ли вы более эффективно переписать свои уравнения? Анализ математики может привести к сокращению: возможно, вы можете переписать некоторые (или все) итераций в закрытой форме.

  • Можете ли вы заменить ваши уравнения в целом чем-то более простым? Например, предположим, вам нужно отсортировать набор мест по их расстоянию от вашего дома. Расчет расстояния требует вычитания, возведения в квадрат, добавления и квадратного корня. В общем случае квадратный корень - самая дорогостоящая операция. Но если вам нужны только относительные расстояния, вы можете пропустить квадратный корень: сортировка по квадрату расстояния порождает тот же порядок сортировки!

  • Если встраивание невозможно, можете ли вы определить свои функции (или их компоненты) как эффективные макросы, так что по крайней мере вы можете избежать повторения кода? Как вы упомянули, наследование буфера обмена является смертельным врагом ремонтопригодности.

Если ничего другого, то, пройдя это упражнение, научит вас размышлениям о том, как работает ваш компилятор и язык C. Удачи!

Ответ 2

В дополнение к профилированию я бы предложил просмотреть код, который на самом деле испускает компилятор (cc -S *.c для многих компиляторов). Это должно рассказать вам, как (или если) цикл разворачивается, а также показывать, какие инварианты цикла перемещаются. Не забудьте указать те же параметры оптимизации, что и для ваших обычных компиляций.

Ответ 3

Мой вопрос заключается в том, что компилятор может надежно расширить его, чтобы...

Ответ, который вы ищете, - нет. Оптимизация компилятора сильно переоценивается, особенно если вы не используете MSVC и не нацеливаете Wintel.

Как уже было сказано, проверьте, чтобы ассемблерный выход для проверки. Лично я бы играл в нее безопасно и просто писал код, поэтому компилятору не нужно делать эту работу для вас - и на самом деле я думаю, что это более читаемо, если вам нужно сделать что-то особенное для первой и последней итераций для рефакторинговых циклов, таких как это все равно... вместо того, чтобы думать о условностях и о том, что они означают, порядок исполнения следует порядку написания кода, что гораздо более естественно (и, как письменный язык).

Теперь вы, кажется, предполагаете, что это было невозможно прочитать - правильное решение imo - не придерживаться всего кода в цикле, а вместо этого переместить нечитаемые блоки кода в хорошо названные функции с сильными подсказками вставки (__forceinline и т.д.), то итерация может выглядеть так:

prepareForIteration( A, B );
for( int i = 1; i < ( end - 1 ); ++i )
{
    iterationStep( i, A, B, C, D );
}
finaliseIteration( end, C, D );

Конечно, поскольку вы знаете, что на самом деле делает код, я уверен, что вы можете найти лучшие имена...

Ответ 4

Лучшее, что вы можете сделать, это то, что вы уже сделали: объедините все четыре массива одновременно, а не через каждый отдельно. Удобные для доступа к кэшу шаблоны являются, безусловно, самой важной микро-оптимизацией при работе с большими массивами.

В примере, который вы даете, я бы попробовал следующее, если вы используете gcc или clang-llvm:

for(i=0;i<end;i++)
{
    if(__builtin_expect((i == 0), 0))
        //calculate A[i+1] and B[i+1]
    else if (__builtin_expect((i == end-1), 0))
        //calculate C[i-1] and D[i-1]
    else
        //calculate A[i+1], B[i+1], C[i-1], D[i-1]
}

Это "подсказка" для компилятора, который он передаст вместе с CPU, чтобы помочь с предсказанием ветвлений. На современном процессоре ошибочная ветвь может стоить сотни циклов. (С другой стороны, на современном процессоре логика предсказания каждой ветки, основанная на том, как часто она принималась в прошлом, довольно сложна. Таким образом, ваш пробег может меняться.) Правильно спрогнозированная ветвь стоит 1 цикл или даже меньше, поэтому это следующее, о чем я буду беспокоиться.

Обратите внимание, что классические методы оптимизации, такие как разворот цикла, почти бесполезны и, вероятно, даже контрпродуктивны.

Наконец, если ваш алгоритм может быть векторизован, то это ваш следующий шаг. Преимущество перехода условных условий из цикла полностью состоит в том, что он может упростить его автоматическое векторное определение. Но если цикл является нетривиальным, вам может потребоваться написать векторный код вручную; попробуйте выполнить поиск "SSE intrinsics".

Как предложили другие, начните с использования профилировщика и опции -S (или эквивалента) для вашего ассемблера. Удачи.