Производительность: мод и назначение против условного и назначение - PullRequest
0 голосов
/ 02 октября 2018

У меня есть счетчик в ISR (который вызывается внешним IRQ при 50us).Счетчик увеличивается и оборачивается вокруг MAX_VAL (240).

У меня есть следующий код:

if(condition){
  counter++;
  counter %= MAX_VAL;
  doStuff(table[counter]);
}

Я рассматриваю альтернативную реализацию:

if(condition){
  //counter++;//probably I would increment before the comparison in production code
  if(++counter >= MAX_VAL){
    counter=0;
  }
  doStuff(table[counter]);
}

IЗнаю, что люди рекомендуют не пытаться оптимизировать таким образом, но это заставило меня задуматься.На х86 что быстрее?какое значение MAX_VAL оправдывает вторую реализацию?

Это вызывается примерно каждые 50us, поэтому сокращение набора инструкций - неплохая идея.If (++ counter> = MAX_VAL) будет предсказано как ложное, поэтому в большинстве случаев оно удалит присвоение 0.Для моих целей id предпочитают согласованность реализации% =.

1 Ответ

0 голосов
/ 02 октября 2018

Как говорит @RossRidge, накладные расходы будут в основном потеряны из-за шума обслуживания прерывания на современном x86 (возможно, по крайней мере, 100 тактов, и много много больше, если это является частью современногоОперационная система с настроенным устранением Meltdown + Spectre).


Если MAX_VAL - это степень 2, counter %= MAX_VAL отлично, особенно если counter без знака (в этом случае просто and, или, в данном случае, movzx байт на слово, которое может иметь нулевую задержку на процессорах Intel. Конечно, при этом у него все равно есть пропускная способность: Может ли MOV x86 действительно быть "свободным"? Почему я не могу воспроизвестиэто вообще? )

Можно ли заполнить последние 255-240 записями чем-то безобидным или повторять что-то?


Пока MAX_VALконстанта времени компиляции , тем не менее, counter %= MAX_VAL будет эффективно компилироваться до пары умножений, сдвигов и сложений.(Опять же, более эффективно для неподписанных.) Почему GCC использует умножение на странное число при реализации целочисленного деления?

Но проверка на повторение еще лучше.Проверка без ответвлений (с использованием cmov) имеет меньшую задержку, чем остаток с использованием мультипликативного обратного, и обходится меньше пропускной способности для пропускной способности.

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

// simple version that works exactly like your question
// further optimizations assume that counter isn't used by other code in the function,
// e.g. making it a pointer or incrementing it for the next iteration
void isr_countup(int condition) {
    static unsigned int counter = 0;

    if(condition){
      ++counter;
      counter = (counter>=MAX_VAL) ? 0 : counter;  // gcc uses cmov
      //if(counter >= MAX_VAL) counter = 0;        // gcc branches
      doStuff(table[counter]);
    }
}

Я скомпилировал много версий этого в проводнике компилятора Godbolt , с последними версиями gcc и clang.

(Подробнее о статическом анализе производительности и пропускной способности для коротких блоков x86 asm см. Какие соображения относятся к прогнозированию задержки для операций на современных суперскалярных процессорах и как их можно вычислить вручную? и другие ссылки в x86 wiki , особенно Руководства Agner Fog .)

clang использует без ветвей cmov для обеих версий.Я скомпилировал с -fPIE на случай, если вы используете это в своих ядрах.Если вы можете использовать -fno-pie, то компилятор может сохранить LEA и использовать mov edi, [table + 4*rcx], предполагая, что вы находитесь на цели, где статические адреса в позиционно-зависимом коде вписываются в 32-битные константы с расширенными знаками (например, true вЯдро Linux, но я не уверен, что они компилируются с -fPIE или делают ASLR ядра с перемещениями при загрузке ядра.)

# clang7.0 -O3 -march=haswell -fPIE.
#  gcc's output is the same (with different registers), but uses `mov edx, 0` before the cmov for no reason, because it's also before a cmp that sets flags
isr_countup:                            # @isr_countup
    test    edi, edi
    je      .LBB1_1                  # if condition is false

    mov     eax, dword ptr [rip + isr_countup.counter]
    add     eax, 1                   # counter++
    xor     ecx, ecx
    cmp     eax, 239                 # set flags based on (counter , MAX_VAL-1)
    cmovbe  ecx, eax                 # ecx = (counter <= MAX_VAL-1) ? 0 : counter
    mov     dword ptr [rip + isr_countup.counter], ecx   # store the old counter
    lea     rax, [rip + table]
    mov     edi, dword ptr [rax + 4*rcx]        # index the table

    jmp     doStuff@PLT             # TAILCALL
.LBB1_1:
    ret

Блок из 8 инструкций, начинающийся с загрузки старойзначение счетчика составляет всего 8 моп (для AMD или Intel Broadwell и более поздних версий, где cmov - только 1 моп)Задержка критического пути от готовности counter к готовности table[++counter % MAX_VAL] составляет 1 (сложение) + 1 (cmp) + 1 (cmov) + задержка использования нагрузки для нагрузки.т.е. 3 дополнительных цикла.Это задержка инструкции 1 mul.Или 1 дополнительный цикл на старом Intel, где cmov равен 2 моп.

Для сравнения, версия с использованием модуля по модулю составляет 14 моп для этого блока с gcc, включая 3-моп mul r32.Задержка составляет не менее 8 циклов, я точно не считал.(Для пропускной способности это только немного хуже, хотя, если более высокая задержка не уменьшает способность неупорядоченного выполнения перекрывать выполнение материала, который зависит от счетчика.)


Другоеоптимизации

  • Используйте старое значение counter и подготовьте значение для следующего раза (снимая вычисления с критического пути.)

  • Используйте указатель вместо счетчика.Сохраняет пару инструкций за счет использования 8 байтов вместо 1 или 4 места в кэше для переменной.(uint8_t counter прекрасно компилируется с некоторыми версиями, просто используя movzx в 64-битную версию).

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

void isr_pointer_inc_after(int condition) {
    static int *position = table;

    if(condition){
        int tmp = *position;
        position++;
        position = (position >= table + MAX_VAL) ? table : position;
        doStuff(tmp);
    }
}

Это очень хорошо компилируется как с gcc, так и с clang, особенно если вывы используете -fPIE, поэтому компилятору в любом случае нужен адрес таблицы в регистре.

# gcc8.2 -O3 -march=haswell -fPIE
isr_pointer_inc_after(int):
    test    edi, edi
    je      .L29

    mov     rax, QWORD PTR isr_pointer_inc_after(int)::position[rip]
    lea     rdx, table[rip+960]        # table+MAX_VAL
    mov     edi, DWORD PTR [rax]       # 
    add     rax, 4
    cmp     rax, rdx
    lea     rdx, -960[rdx]             # table, calculated relative to table+MAX_VAL
    cmovnb  rax, rdx
    mov     QWORD PTR isr_pointer_inc_after(int)::position[rip], rax

    jmp     doStuff(int)@PLT
.L29:
    ret

Снова 8 моп (при условии, что cmov - 1 моп).Это имеет даже более низкую задержку, чем, возможно, могла бы иметь версия счетчика, потому что режим адресации [rax] (с RAX, приходящимся на нагрузку) имеет на 1 цикл меньшую задержку, чем индексированный режим адресации, в семействе Sandybridge.Без смещения он никогда не переносит штраф, описанный в Существует ли штраф, если база + смещение находятся на странице, отличной от базы?

  • Или (сcounter) count down к нулю: потенциально сохраняет инструкцию, если компилятор может использовать флаги, установленные декрементом, для обнаружения отрицательного значения.Или мы всегда можем использовать уменьшенное значение и выполнить перенос после этого: поэтому, когда counter равен 1, мы будем использовать table[--counter] (table[0]), но хранить counter=MAX_VAL.Опять же, снимает проверку обтекания критического пути.

    Если вы хотели версию ветвления, вы бы хотели, чтобы она разветвлялась на флаге переноса, потому что sub eax,1 / jc может слиться макросом в 1упс, но sub eax,1 / js не может слиться с макросами в семействе Сэндибридж. x86_64 - Сборка - условия цикла и выход из строя .Но с без филиалов, это нормально.cmovs (MOV, если установлен флаг знака, т. Е. Если последний результат был отрицательным), так же эффективен, как cmovc (MOV, если установлен флаг переноса).

    Было сложно получить gcc, чтобы использоватьПометить результаты из dec или sub, не делая также cdqe, чтобы расширить индекс до ширины указателя.Думаю, я мог бы использовать счетчик intptr_t, но это было бы глупо;может также просто использовать указатель.С неподписанным счетчиком gcc и clang оба хотят сделать еще один cmp eax, 239 или что-то после декремента, даже если флаги уже установлены очень хорошо из декремента.Но мы можем заставить gcc использовать SF, проверив (int)counter < 0:

    // Counts downward, table[] entries need to be reversed
    void isr_branchless_dec_after(int condition) {
        static unsigned int counter = MAX_VAL-1;
    
        if(condition){
            int tmp = table[counter];
            --counter;
            counter = ((int)counter < 0) ? MAX_VAL-1 : counter;
            //counter = (counter >= MAX_VAL) ? MAX_VAL-1 : counter;
            //counter = (counter==0) ? MAX_VAL-1 : counter-1;
            doStuff(tmp);
        }
    }
    
     # gcc8.2 -O3 -march=haswell -fPIE
    isr_branchless_dec_after(int):
        test    edi, edi
        je      .L20
    
        mov     ecx, DWORD PTR isr_branchless_dec_after(int)::counter[rip]
        lea     rdx, table[rip]
        mov     rax, rcx                   # stupid compiler, this copy is unneeded
        mov     edi, DWORD PTR [rdx+rcx*4] # load the arg for doStuff
        mov     edx, 239                   # calculate the next counter value
        dec     eax
        cmovs   eax, edx
        mov     DWORD PTR isr_branchless_dec_after(int)::counter[rip], eax  # and store it
    
        jmp     doStuff(int)@PLT
    .L20:
        ret
    

    по-прежнему 8 мопов (должно быть 7), но без дополнительной задержки на критическом пути.Таким образом, все дополнительные инструкции декремента и переноса представляют собой сочный параллелизм на уровне команд для выполнения не по порядку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...