Производительность цикла кода C - PullRequest
40 голосов
/ 03 апреля 2012

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

Я использую Intel Core i7-960 (тактовая частота 3,2 ГГц) и уже вручную внедрил ядро ​​с использованием встроенных функций SSE:

 for(int i=0; i<iterations; i+=4) {
    y1 = _mm_set_ss(output[i]);
    y2 = _mm_set_ss(output[i+1]);
    y3 = _mm_set_ss(output[i+2]);
    y4 = _mm_set_ss(output[i+3]);

    for(k=0; k<ksize; k++){
        for(l=0; l<ksize; l++){
            w  = _mm_set_ss(weight[i+k+l]);

            x1 = _mm_set_ss(input[i+k+l]);
            y1 = _mm_add_ss(y1,_mm_mul_ss(w,x1));
            …
            x4 = _mm_set_ss(input[i+k+l+3]);
            y4 = _mm_add_ss(y4,_mm_mul_ss(w,x4));
        }
    }
    _mm_store_ss(&output[i],y1);
    _mm_store_ss(&output[i+1],y2);
    _mm_store_ss(&output[i+2],y3);
    _mm_store_ss(&output[i+3],y4);
 }

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

Производительность этого ядра на моей машине составляет ~ 1.6 операций FP за цикл, в то время как максимум будет составлять 2 операции FP за цикл (поскольку FP add + FP mul может выполняться параллельно).

Если я не ошибаюсь в изучении сгенерированного ассемблерного кода, идеальное расписание будет выглядеть следующим образом, где инструкция mov занимает 3 цикла, задержка переключения с домена загрузки на домен FP для зависимогоинструкция занимает 2 цикла, умножение FP - 4 цикла, а добавление FP - 3 цикла.(Обратите внимание, что зависимость от умножения -> сложение не приводит к задержке переключения, поскольку операции принадлежат одному домену).

schedule

В соответствии с измеренной производительностью (~80% от максимальной теоретической производительности) накладные расходы составляют ~ 3 инструкции на 8 циклов.

Я пытаюсь либо:

  • избавиться от этих накладных расходов, либо
  • объяснить, откуда они

КонечноСуществует проблема с пропуском кеша и смещением данных, которые могут увеличить задержку команд перемещения, но есть ли другие факторы, которые могут играть роль здесь?Как регистр чтения киосков или что-то?

Я надеюсь, что моя проблема ясна, заранее спасибо за ваши ответы!


Обновление: сборка внутреннего цикла выглядит следующим образом:

...
Block 21: 
  movssl  (%rsi,%rdi,4), %xmm4 
  movssl  (%rcx,%rdi,4), %xmm0 
  movssl  0x4(%rcx,%rdi,4), %xmm1 
  movssl  0x8(%rcx,%rdi,4), %xmm2 
  movssl  0xc(%rcx,%rdi,4), %xmm3 
  inc %rdi 
  mulss %xmm4, %xmm0 
  cmp $0x32, %rdi 
  mulss %xmm4, %xmm1 
  mulss %xmm4, %xmm2 
  mulss %xmm3, %xmm4 
  addss %xmm0, %xmm5 
  addss %xmm1, %xmm6 
  addss %xmm2, %xmm7 
  addss %xmm4, %xmm8 
  jl 0x401b52 <Block 21> 
...

Ответы [ 3 ]

30 голосов
/ 03 апреля 2012

Я заметил в комментариях, что:

  • Для выполнения цикла требуется 5 циклов.
  • Предполагается, что это займет 4 цикла. (так как есть 4 добавления и 4 множителя)

Однако, ваша сборка показывает 5 инструкций SSE movssl. Согласно таблицам Агнера Фога все инструкции перемещения SSE с плавающей запятой имеют, по крайней мере, 1 инст / цикл обратная пропускная способность для Nehalem.

Так как у вас их 5, вы не можете сделать лучше, чем 5 циклов / итерация .


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

Один из распространенных подходов - использовать tiling . Где вы добавляете уровни вложения, чтобы улучшить местность. Хотя он в основном используется для улучшения доступа к кешу, его также можно использовать в регистрах, чтобы уменьшить количество необходимых загрузок / хранилищ.

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

1 голос
/ 04 апреля 2012

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

for(int i=0; i<size; i+=16) {
    y1 = _mm_load_ps(output[i]);
    …
    y4 = _mm_load_ps(output[i+12]);

    for(k=0; k<ksize; k++){
        for(l=0; l<ksize; l++){
            w  = _mm_set_ps1(weight[i+k+l]);

            x1 = _mm_load_ps(input[i+k+l]);
            y1 = _mm_add_ps(y1,_mm_mul_ps(w,x1));
            …
            x4 = _mm_load_ps(input[i+k+l+12]);
            y4 = _mm_add_ps(y4,_mm_mul_ps(w,x4));
        }
    }
    _mm_store_ps(&output[i],y1);
    …
    _mm_store_ps(&output[i+12],y4);
    }

Измеренная производительность этого ядра составляет около 5,6 операций FP за цикл, хотя я ожидаюэто должно быть ровно в 4 раза больше производительности скалярной версии, т.е. 4,1,6 = 6,4 операций в секунду за цикл.

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

schedule

Похоже, что график неt, хотя после операции movss есть дополнительная инструкция, которая перемещает значение скалярного веса в регистр XMM, а затем использует shufps для копирования этого скалярного значения во весь вектор.Кажется, что вектор весов готов к использованию в течение mulps времени, принимая во внимание задержку переключения с нагрузки на домен с плавающей запятой, поэтому это не должно вызывать дополнительных задержек.

The *Инструкции 1016 * (выровненный, упакованный ход), * ​​1017 * & mulps, которые используются в этом ядре (проверено с помощью кода сборки), имеют такую ​​же задержку и пропускную способность, что и их скалярные версии, поэтому это также не должно вызывать дополнительной задержки.

У кого-нибудь есть идея, на что тратится этот дополнительный цикл на 8 циклов, если предположить, что максимальная производительность, которую может получить это ядро, составляет 6,4 FP на цикл, а скорость его работы составляет 5,6 FP на цикл?

Еще раз спасибо за вашу помощь!

0 голосов
/ 03 апреля 2012

Создание этого ответа из моего комментария.

В несерверном дистрибутиве Linux я считаю, что таймер прерывания обычно установлен на 250 Гц по умолчанию, хотя в разных дистрибутивах он почти всегда превышает 150. Эта скоростьнеобходимо предоставить интерактивный графический интерфейс со скоростью 30 + кадров в секунду.Этот таймер прерывания используется для вытеснения кода.Это означает, что 150+ раз в секунду ваш код прерывается, и код планировщика запускается и решает, что уделить больше времени.Похоже, вы отлично справляетесь, просто набирая 80% максимальной скорости, без проблемЕсли вам нужно лучше установить, скажем, Ubuntu Server (100 Гц по умолчанию) и немного подкорректировать ядро ​​(выгрузка прервана)

РЕДАКТИРОВАТЬ: В системе с ядром 2+ это оказывает гораздо меньшее влияние, так как ваш процесс почти наверняка будет шлепанна одно ядро ​​и более или менее осталось сделать свое дело.

...