Как оптимизировать цикл C?

У меня проблема с производительностью в узком месте в моем коде. В основном это простой вложенный цикл.

Профилирование проблемы показывает, что программа тратит много времени, просто увеличивая оба счетчика циклов (++) и тестирование для завершения (i/j < 8).

Наблюдая за выходом сборки, я вижу, что оба счетчика не получают регистров, и доступ к ним стоит много циклов. Использование ключевого слова "register" не убеждает компилятор фактически помещать их в регистры. есть ли что-то, что можно сделать для оптимизации времени доступа счетчиков?

Здесь вывод сборки. Источник C - это простой вложенный цикл с i/j счетчиками.

  2738  0.2479  2459  0.1707   :    1e6c:   jne    1dd1 <process_hooks+0x121>
  1041  0.0942  1120  0.0778   :    1e72:   addl   $0x1,0xffffffd4(%ebp)
  2130  0.1928  2102  0.1459   :    1e76:   cmpl   $0x8,0xffffffd4(%ebp)
  2654  0.2403  2337  0.1622   :    1e7a:   jne    1da0 <process_hooks+0xf0>
  809   0.0732   814  0.0565   :    1e80:   jmp    1ce2 <process_hooks+0x32>

В соответствии с запросом здесь также используется код C. Компилятор - gcc btw:

for (byte_index=0; byte_index < MASK_SIZE / NBBY; byte_index++)
{
    if (check_byte(mask,byte_index))
    {
        for (bit_index=0; bit_index < NBBY; bit_index++)
        {
            condition_index = byte_index*NBBY + bit_index;
            if (check_bit(condition_mask,condition_index))
            {
                .
                .
                .
            }
        }
    }
}

Спасибо

Ответы

Ответ 1

Есть две возможные причины, по которым он не попадает в регистр:

Переменная должна храниться в памяти

Если вы берете адрес переменной или объявляете ее изменчивой, ее не хранят в регистре. Это не похоже на то, что вы делаете это, но это может произойти в разделе ....

gcc плохо работает в распределении регистров.

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

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

Вы также можете попробовать gcc попробовать сложнее, с атрибутом hot.

Наконец, вы также можете настроить файлы с помощью gcc --param. Они выставляют внутренние настройки компилятора, поэтому это, вероятно, не следует начинать с малого.

Ответ 2

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

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

Ответ 3

Лучшие результаты (скорость), которую я получаю при использовании компилятора Intel.

Вы правы в том, что ключевое слово "register" используется только как подсказка для компилятора (так же как inline).

Если вы действительно думаете, что этот цикл является основным узким местом, просто введите raw assembly. Я знаю, что это вряд ли переносится, но опять же, обычно это не имеет большого значения, и если это должно быть переносимым... это только в одном конкретном месте.

вы можете даже #ifdef весь бит с исходным кодом C для поддержания переносимости

Ответ 4

    for (bit_index=0; bit_index < NBBY; bit_index++)
    {
        condition_index = byte_index*NBBY + bit_index;
        if (check_bit(condition_mask,condition_index))
        {
            .
            .
            .
        }
    }

может быть так же легко:

    condition_index = byte_index * NBBY;
    for (bit_index=0; bit_index < NBBY; bit_index++, condition_index++)
    {
        if (check_bit(condition_mask,condition_index))
        {
            .
            .
            .
        }
    }

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

Говоря о правильном объеме, нет причин объявлять счетчики циклов вне цикла. Этот стиль C устарел в течение многих лет, и, хотя он, вероятно, не является недостатком производительности, ограничение вещей до наименьшей логической области приводит к более чистым и более удобному коду.

Для 8 бит вы, вероятно, можете развернуть, но в зависимости от вашего оборудования это может работать не очень хорошо. Есть много других способов, которыми вы могли бы это сделать, я, вероятно, пропустил несколько взглядов на это. В аппаратных средствах, которые я работал с условными выражениями внутри циклов, обычно яд для производительности, но я не вижу очевидного способа избежать этого. Я бы, конечно, рассмотрел итерацию по битам, а не байтам во внешнем цикле, чтобы избежать умножения в общем случае. Просто предлагаю это... Я думаю, что в этом случае не было бы явного преимущества.

Ответ 5

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

Ответ 6

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

Я предполагаю, что это во многом зависит от вашего уровня компилятора и оптимизации. Как говорили другие, это может быть хорошим кандидатом для -funroll-all-loops (gcc).

Ответ 7

Это может показаться второстепенным, но вместо использования формы: index ++, используйте ++ index;

Обоснование заключается в том, что индекc++ требует кэширования текущего значения r перед его увеличением, тогда как индекc++ возвращает вновь вычисленное значение rvalue, которое уже должно быть кэшировано, тем самым сохраняя ссылку.

Конечно, хороший компилятор оптимизирует это, так что это, вероятно, не проблема.

Ответ 8

Я надеюсь, что эти 2 функции встроены (check_bit и check_byte), поскольку они намного медленнее, чем любая переменная регистра может сделать ваш цикл.

если компилятор не строит их, вставьте их в цикл.

Ответ 9

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

EDIT: Еще одна вещь, которую следует учитывать, если вы действительно хотите сделать часть кода быстрее, вы можете использовать специальные инструкции CPU, ваш компилятор, вероятно, будет неясно, когда их использовать. Например, в Intel есть много инструкций, которые можно использовать, вплоть до SSE4 и более, это действительно то, где вы можете лучше работать с вашим компилятором, так как он не знает, чего вы хотите достичь на уровне алгоритма. Ознакомьтесь с руководством по разработке программного обеспечения Intel (R) 64 и IA-32. Также на этом уровне вы можете получить лучший контроль над трубопроводом.

Если вы не хотите писать сборку, иногда есть инструкции для инструкций, которые будут использоваться в 'C'.

Что касается проверки, включен ли бит: Не уверен, что вы хотите сделать, если бит включен, но (если ваши биты выровнены по байт):

Предположим, вы получите байт 0110 0110 на X. Вы захотите сделать что-нибудь, возможно, напечатайте такой массаж, как "Биты 1,2,5,6 включены". Вы можете создать 256 функций, каждый из которых сделает что-то вроде отображения такого вида массажа. Как вы узнаете, какой из них активировать? Оболочка номера функции - это точно значение полученного байта, поэтому вы можете просто использовать [], чтобы перейти туда. однако это будет таблица указателей на функции. Он должен выглядеть примерно так:

//define the functions
void func0()
{
   printf("No Bits are on.");
}

void func1()
{
   printf("Bit 0 is on.");
}
.
.
.

//create the table
void  (*table[256])();
table[0] = &func0;
table[1] = &func1;
.
.
.

//the for loop
void  (*pointer_to_func)();
for...
{
   X = getByte();
   pointer_to_func = table[X]; //table shell contain 256 function pointers.
   pointer_to_func(); //call the function
}

это должно вызывать функцию в позиции X и выполнять ее, я предполагаю, что функция в местоположении X == 102 (десятичная цифра 0110 0110) будет выглядеть примерно так:

printf ( "Биты 1,2,5,6 включены" );

Смотрите Учебники по указателям функций , Specifacly this.

Ответ 10

Вы можете попробовать переформатировать его до одного индекса и посмотреть, не изменит ли он компилятор:

for (condition_index = 0; condition_index < MASK_SIZE;)
{
    if (check_byte(mask, condition_index / NBBY))
    {
        for (bound = condition_index + NBBY; condition_index < bound; condition_index++)
        {
            if (check_bit(condition_mask, condition_index))
            {
                /* stuff */
            }
        }
    }
    else
    {
        condition_index += NBBY;
    }
}

(Надеемся, что NBBY - это сила 2, поэтому разделение будет реализовано как сдвиг)

Ответ 11

Если верно, что /* stuff */code внутри внутреннего if() выполняется редко (и существует априорная известная оценка количества раз, когда это может произойти или, по крайней мере, разумного предела) это может быть улучшением производительности для перехода на двухпроходное решение. Это устранит давление регистра в вложенных циклах. Ниже приведен мой предыдущий одноиндексный ответ:

for (n = 0, condition_index = 0; condition_index < MASK_SIZE;)
{
    if (check_byte(mask, condition_index / NBBY))
    {
        for (bound = condition_index + NBBY; condition_index < bound; condition_index++)
        {
            if (check_bit(condition_mask, condition_index))
            {
                condition_true[n++] = condition_index;
            }
        }
    }
    else
    {
        condition_index += NBBY;
    }
}

do {
    condition_index = condition_true[--n];
    /* Stuff */
} while (n > 0);

Ответ 12

Вы можете попробовать развернуть цикл. Компилятор может сделать это за вас, но если нет, и вам действительно нужна производительность, сделайте это самостоятельно. Я предполагаю, что вы делаете что-то вроде вызова function(.., i, j, ..) каждой итерации, поэтому просто замените циклы:

function(.., 0, 0, ..)
...
function(.., 0, 7, ..)
function(.., 1, 0, ..)
...
function(.., 7, 7, ..)

С дополнительным контекстом (источник C) могут быть более полезные вещи. Честно говоря, я был бы шокирован, если бы два стековых подсчета (многие современные процессоры имеют специальное ускорительное оборудование для доступа к верхнему биту стека почти так же быстро, как и регистры) вызывают заметную проблему в не игрушечной программе.

Ответ 13

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

Ответ 14

Предполагая, что информация профилирования верна, и действительно это операции с приращением, которые вызывают узкое место, вы могли бы злоупотреблять некоторым исполнением вне порядка:

for (byte_index = 0; byte_index < MASK_SIZE / NBBY; )
{
    if (check_byte(mask,byte_index++))
    {
        condition_index = byte_index*NBBY;
        for (bit_index=0; bit_index < NBBY; )
        {
            if (check_bit(condition_mask,condition_index + bit_index++))
            {
                ...
            }
        }
    }
}

(приведенный выше фрагмент не будет работать по понятным причинам, но вы должны получить идею:)

Кроме того, из имен функций/макросов в вашем фрагменте кода C, я предполагаю, что вы работаете с бит-масками, чтобы делать что-то. Одна вещь, которая помогла мне улучшить производительность раньше, - это итерация по массиву масок, а не выполнение динамических вычислений на входе, то есть нечто вроде

for (byte_index = 0; byte_index < MASK_SIZE / NBBY; byte_index++)
{
    if (check_byte(mask,byte_index))
    {
        const char masks[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
        for (mask_index=0; mask_index < sizeof(masks) / sizeof(masks[0]); mask_index++)
        {
            if (check_bit(masks[mask_index], byte_index))
            {
                ...
            }
        }
    }
}

... что у компилятора может быть больше шансов правильно оптимизировать/развернуть.

Ответ 15

Не видя, что находится во внутреннем цикле, нет смысла пытаться оптимизировать петли. Похоже, код генерируется для x86 32bit. Если для вычисления в цикле требуется несколько регистров, нет смысла компилятору удерживать счетчик циклов в регистрах, так как ему все равно придется разливать их в стек. Тогда в зависимости от инструкций, используемых во внутреннем цикле, могут возникнуть некоторые проблемы с распределением регистров. Сдвиги используют регистр ECX только в том случае, когда счетчик, умножение и деление имеют ограничения в отношении используемых регистров, строковые команды используют ESI и EDI в качестве регистров, что уменьшает возможность компилятора хранить в них значения. И, как уже было сказано, вызов в середине цикла тоже не помогает, так как регистры должны быть сохранены в любом случае.

Ответ 16

Я попытался бы разобраться с проблемой по-разному. Что именно делает код, возможно, объясняя, что он делает, можно использовать другой алгоритм, который более эффективен?

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