Микрооптимизация функции сравнения C++

У меня есть функция Compare(), которая выглядит так:

inline bool Compare(bool greater, int p1, int p2) {
  if (greater) return p1>=p2;
  else return p1<=p2;
}

Я решил оптимизировать, чтобы избежать ветвления:

inline bool Compare2(bool greater, int p1, int p2) {
  bool ret[2] = {p1<=p2,p1>=p2};
  return ret[greater];
}

Затем я проверил это:

bool x = true;
int M = 100000;
int N = 100;

bool a[N];
int b[N];
int c[N];

for (int i=0;i<N; ++i) {
  a[i] = rand()%2;
  b[i] = rand()%128;
  c[i] = rand()%128;
}

// Timed the below loop with both Compare() and Compare2()
for (int j=0; j<M; ++j) {
  for (int i=0; i<N; ++i) {
    x ^= Compare(a[i],b[i],c[i]);
  }
}

Результаты:

Compare(): 3.14ns avg
Compare2(): 1.61ns avg

Я бы сказал, дело закрыто, избегайте ветвления FTW. Но для полноты я заменил

a[i] = rand()%2;

с:

a[i] = true;

и получил точно такое же измерение ~ 3,14 нс. Предположительно, тогда ветвление не происходит, и компилятор фактически переписывает Compare(), чтобы избежать оператора if. Но тогда почему Compare2() быстрее?

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

EDIT: Ниже приведена сборка:

_Z7Comparebii:
.LFB4:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -8(%rbp)
    movl    %edx, -12(%rbp)
    movb    %al, -4(%rbp)
    cmpb    $0, -4(%rbp)
    je      .L2
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setge   %al
    jmp     .L3
.L2:
    movl    -8(%rbp), %eax
    cmpl    -12(%rbp), %eax
    setle   %al
.L3:
    leave
    ret
    .cfi_endproc
.LFE4:
    .size   _Z7Comparebii, .-_Z7Comparebii
    .section        .text._Z8Compare2bii,"axG",@progbits,_Z8Compare2bii,comdat
    .weak   _Z8Compare2bii
    .type   _Z8Compare2bii, @function
_Z8Compare2bii:
.LFB5:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    pushq   %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    movl    %edi, %eax
    movl    %esi, -24(%rbp)
    movl    %edx, -28(%rbp)
    movb    %al, -20(%rbp)
    movw    $0, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setle   %al
    movb    %al, -16(%rbp)
    movl    -24(%rbp), %eax
    cmpl    -28(%rbp), %eax
    setge   %al
    movb    %al, -15(%rbp)
    movzbl  -20(%rbp), %eax
    cltq
    movzbl  -16(%rbp,%rax), %eax
    leave
    ret
    .cfi_endproc
.LFE5:
    .size   _Z8Compare2bii, .-_Z8Compare2bii
    .text

Теперь фактический код, выполняющий тест, может использовать встроенные версии двух вышеупомянутых функций, поэтому есть вероятность, что это может быть неправильный код для анализа. При этом я вижу команду jmp в Compare(), поэтому я думаю, что это означает, что она разветвляется. Если это так, я думаю, возникает вопрос: почему предсказатель ветвления не улучшает производительность Compare(), когда я меняю a[i] с rand()%2 на true (или false в этом отношении)?

EDIT2: я заменил "предсказание ветвления" на "ветвление", чтобы сделать мой пост более осмысленным.


person dshin    schedule 02.04.2013    source источник
comment
optimize to avoid branch prediction Разве это не оксюморон?   -  person Lightness Races in Orbit    schedule 02.04.2013
comment
Вам придется поделиться ассемблерным кодом, так как то, что произойдет, во многом зависит от того, какой компилятор вы используете и на каком уровне оптимизации.   -  person Raymond Chen    schedule 02.04.2013
comment
@ Last Line: тогда почему сборку не выкладываешь?   -  person    schedule 02.04.2013
comment
Вы не установили семя. Может быть, компилятор достаточно умен, чтобы знать, что возвращает rand() в этом случае? Просто быстрая мысль. Также вы должны действительно сравнить сборку. Даже если вы неграмотны в ассемблере, вы все равно можете показать разницу.   -  person Zeta    schedule 02.04.2013
comment
Возможно был условный ход.. покажи сборку.   -  person harold    schedule 02.04.2013
comment
Трудно сказать, не видя, что делает компилятор.   -  person Mysticial    schedule 02.04.2013
comment
какая у вас арка и компилятор?   -  person Luka Rahne    schedule 02.04.2013
comment
Каково время для a[i] = false? Быстрее, чем 1,61 нс?   -  person SinisterMJ    schedule 02.04.2013
comment
@LukaRahne: я использую g++ 4.4.3-4ubuntu5 на Intel Xeon X5570. @другие: потерпите меня, пока я выясняю, как опубликовать сборку.   -  person dshin    schedule 02.04.2013
comment
@AntonRoth Compare() и Compare2() работали с частотой ~ 3,14 нс и ~ 1,61 нс соответственно во всех трех случаях a[i]: rand(), true и false.   -  person dshin    schedule 02.04.2013
comment
Подождите, что быстрее? В вашем последнем комментарии только что говорилось, что Compare() быстрее, но в вашем вопросе все наоборот.   -  person Mysticial    schedule 02.04.2013
comment
Не беспокойтесь об ответе на мой комментарий.   -  person Lightness Races in Orbit    schedule 02.04.2013
comment
Просто для подтверждения, это скомпилировано с -O3?   -  person Oliver Charlesworth    schedule 02.04.2013
comment
Кстати, чего вы хотите избежать, так это ветвей, а не предсказания ветвлений - предсказание ветвлений - это хорошо.   -  person Mark Ransom    schedule 02.04.2013
comment
@Mysticial Compare2() быстрее, я поспешно набрал свой комментарий для AntonRoth, а затем быстро отредактировал его.   -  person dshin    schedule 02.04.2013
comment
@OliCharlesworth Да, с -O3   -  person dshin    schedule 02.04.2013
comment
@MarkRansom: Если только он не предсказывает неправильно в большом проценте случаев ...   -  person Oliver Charlesworth    schedule 02.04.2013
comment
В этом случае термин будет заключаться в том, чтобы избежать неправильных предсказаний ветвей, что также можно сделать, полностью избегая ветвей.   -  person Mysticial    schedule 02.04.2013
comment
Да. В этом случае предсказание перехода будет ошибочным в большом проценте случаев.   -  person drescherjm    schedule 02.04.2013
comment
@OliCharlesworth, даже если бы это было неправильно в 100% случаев, это было бы не хуже, чем остановка конвейера, не так ли? И на самом деле это не должно быть ошибкой более чем на 50%.   -  person Mark Ransom    schedule 02.04.2013
comment
Вопрос, заданный в первом комментарии к этому вопросу, который все счастливо игнорируют, хуже ли неправильное предсказание, чем отсутствие предсказания?   -  person Lightness Races in Orbit    schedule 02.04.2013
comment
@MarkRansom Что ж, если вы сравните два почти идентичных процессора: один с прогнозированием ветвлений и один без него, то вполне вероятно, что процессор без него будет быстрее, чем процессор с ним, и будет ошибаться в 100% случаев. Это связано с тем, что очистка от неправильного предсказания имеет значительные накладные расходы.   -  person Mysticial    schedule 02.04.2013
comment
@ Дэвид заметил инструкции setge и setle? Они обеспечивают логический результат сравнения без каких-либо ветвлений. Обе версии используют эти инструкции. Не отвечает на ваш вопрос, но я подумал, что вам будет интересно.   -  person Mark Ransom    schedule 02.04.2013
comment
Не могли бы вы попробовать (rand()/256)%2 вместо rand()%2? Младший значащий бит rand() может быть хорошо предсказуем и так же хорош, как true или false для предсказателя ветвления.   -  person Evgeny Kluev    schedule 02.04.2013
comment
@MarkRansom: Это правда, для случайных данных можно было бы ожидать 50% ошибочных прогнозов (я думаю). Чтобы вызвать 100% неверное предсказание, должен быть определенный патологический ввод.   -  person Oliver Charlesworth    schedule 02.04.2013
comment
Я попробовал (rand()%256)<128 вместо rand()%2, тот же результат.   -  person dshin    schedule 02.04.2013
comment
Легко понять, почему обе функции работают одинаково для случайного/постоянного первого параметра: ни одна из них не использует инструкции ветвления (при встраивании/оптимизации). Но я понятия не имею, почему Compare2 быстрее. На самом деле единственная разница в том, что Compare использует инструкцию cmov, а Compare2 использует в два раза больше обращений к памяти. Я ожидаю, что Compare2 будет в два раза медленнее, чем Compare...   -  person Evgeny Kluev    schedule 03.04.2013
comment
@Evgeny Очевидно, что в Compare() происходит ветвь - по крайней мере, когда она не встроена (и я не понимаю, почему встраивание может помочь, если компилятор не может вывести значение большего). g++ 4.5.3 под cygwin создает для меня ожидаемый код без сюрпризов (слишком много движений происходит в данной сборке для меня)   -  person Voo    schedule 03.04.2013


Ответы (3)


Я думаю, что понял большую часть этого.

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

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

Compare(): 7.18ns avg
Compare2(): 3.15ns avg

Затем, когда я заменил a[i]=rand()%2 на a[i]=false, я получил следующее:

Compare(): 2.59ns avg
Compare2(): 3.16ns avg

Это демонстрирует выигрыш от предсказания ветвлений. Тот факт, что замена a[i] не дала никаких улучшений, изначально показывает, что встраивание удалило ветвь.

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

Спасибо за множество полезных комментариев.

РЕДАКТИРОВАТЬ: я должен добавить, что вероятная причина того, что Compare2 превосходит все остальные, заключается в том, что процессор может выполнять оба сравнения параллельно. Это была интуиция, которая привела меня к тому, как я написал функцию. Все другие варианты по существу требуют двух логически последовательных операций.

person dshin    schedule 02.04.2013

Я написал библиотеку C++ под названием Celero, предназначенную для тестирования именно таких оптимизаций и альтернатив. (Беззастенчивая самореклама: https://github.com/DigitalInBlue/Celero)

Я запустил ваши случаи, используя следующий код:

class StackOverflowFixture : public celero::TestFixture
{
  public:
    StackOverflowFixture()
    {
    }

    inline bool NoOp(bool greater, int p1, int p2) 
    {
      return true;
    }

    inline bool Compare(bool greater, int p1, int p2) 
    {
      if(greater == true)
      {
        return p1>=p2;
      }

      return p1<=p2;
    }

    inline bool Compare2(bool greater, int p1, int p2)
    {
      bool ret[2] = {p1<=p2,p1>=p2};
      return ret[greater];
    }

    inline bool Compare3(bool greater, int p1, int p2) 
    {
      return (!greater != !(p1 <= p2)) | (p1 == p2);
    }

    inline bool Compare4(bool greater, int p1, int p2) 
    {
      return (greater ^ (p1 <= p2)) | (p1 == p2);
    }
};

BASELINE_F(StackOverflow, Baseline, StackOverflowFixture, 100, 5000000)
{
  celero::DoNotOptimizeAway(NoOp(rand()%2, rand(), rand()));
}

BENCHMARK_F(StackOverflow, Compare, StackOverflowFixture, 100, 5000000)
{
  celero::DoNotOptimizeAway(Compare(rand()%2, rand(), rand()));
}

BENCHMARK_F(StackOverflow, Compare2, StackOverflowFixture, 100, 5000000)
{
  celero::DoNotOptimizeAway(Compare2(rand()%2, rand(), rand()));
}

BENCHMARK_F(StackOverflow, Compare3, StackOverflowFixture, 100, 5000000)
{
  celero::DoNotOptimizeAway(Compare3(rand()%2, rand(), rand()));
}

BENCHMARK_F(StackOverflow, Compare4, StackOverflowFixture, 100, 5000000)
{
  celero::DoNotOptimizeAway(Compare4(rand()%2, rand(), rand()));
}

Результаты показаны ниже:

[==========]
[  CELERO  ]
[==========]
[ STAGE    ] Baselining
[==========]
[ RUN      ] StackOverflow.Baseline -- 100 samples, 5000000 calls per run.
[     DONE ] StackOverflow.Baseline  (0.690499 sec) [5000000 calls in 690499 usec] [0.138100 us/call] [7241140.103027 calls/sec]
[==========]
[ STAGE    ] Benchmarking
[==========]
[ RUN      ] StackOverflow.Compare -- 100 samples, 5000000 calls per run.
[     DONE ] StackOverflow.Compare  (0.782818 sec) [5000000 calls in 782818 usec] [0.156564 us/call] [6387180.672902 calls/sec]
[ BASELINE ] StackOverflow.Compare 1.133699
[ RUN      ] StackOverflow.Compare2 -- 100 samples, 5000000 calls per run.
[     DONE ] StackOverflow.Compare2  (0.700767 sec) [5000000 calls in 700767 usec] [0.140153 us/call] [7135039.178500 calls/sec]
[ BASELINE ] StackOverflow.Compare2 1.014870
[ RUN      ] StackOverflow.Compare3 -- 100 samples, 5000000 calls per run.
[     DONE ] StackOverflow.Compare3  (0.709471 sec) [5000000 calls in 709471 usec] [0.141894 us/call] [7047504.408214 calls/sec]
[ BASELINE ] StackOverflow.Compare3 1.027476
[ RUN      ] StackOverflow.Compare4 -- 100 samples, 5000000 calls per run.
[     DONE ] StackOverflow.Compare4  (0.712940 sec) [5000000 calls in 712940 usec] [0.142588 us/call] [7013212.893091 calls/sec]
[ BASELINE ] StackOverflow.Compare4 1.032500
[==========]
[ COMPLETE ]
[==========]

Учитывая этот тест, похоже, что Compare2 — лучший вариант для этой микрооптимизации.

РЕДАКТИРОВАТЬ:

Сборка Compare2 (лучший случай):

cmp r8d, r9d
movzx   eax, dl
setle   BYTE PTR ret$[rsp]
cmp r8d, r9d
setge   BYTE PTR ret$[rsp+1]
movzx   eax, BYTE PTR ret$[rsp+rax]

Compare3 Assembly (следующий лучший случай):

xor r11d, r11d
cmp r8d, r9d
mov r10d, r11d
setg    r10b
test    dl, dl
mov ecx, r11d
sete    cl
mov eax, r11d
cmp ecx, r10d
setne   al
cmp r8d, r9d
sete    r11b
or  eax, r11d
person DigitalInBlue    schedule 03.04.2013
comment
Интересно, но здесь мы хотим знать, почему это так. - person J.N.; 03.04.2013
comment
Я добавил сборку в свой ответ. - person DigitalInBlue; 03.04.2013
comment
Я не фанат того, как вы проводили бенчмаркинг. В измеренном времени преобладает стоимость rand(), что маскирует реальную разницу в производительности между вариантами. - person dshin; 03.04.2013
comment
Это правда, что rand() стоит дорого, но стоимость одинакова для каждого теста, поэтому ее можно исключить. Что следует сравнивать, так это базовое (относительное) время. Это показывает, что действительно быстрее и насколько. Измерение среднего времени выполнения на самом деле некорректно. Ссылка: codeproject.com/Articles/525576/ - person DigitalInBlue; 03.04.2013
comment
Учитывая базовый уровень, Compare2 в 1,014870 раз медленнее, чем базовое измерение, а Compare3 медленнее в 1,027476 раз. - person DigitalInBlue; 03.04.2013
comment
Ага, понятно. Думаю, я бы предпочел увидеть утверждение: Compare2 в 1,85 раза быстрее, чем Compare3 (поскольку (1,027476-1)/(1,014870-1) = 1,85). На мой взгляд, то, как сообщаются ваши цифры, делает величину улучшения неочевидной. - person dshin; 03.04.2013

Как насчет этого...

inline bool Compare3(bool greater, int p1, int p2) 
{
  return (!greater != !(p1 <= p2)) | (p1 == p2);
}

or

inline bool Compare4(bool greater, int p1, int p2) 
{
  return (greater ^ (p1 <= p2)) | (p1 == p2);
}
person Sharath    schedule 02.04.2013
comment
Мне кажется, что Compare3(true,1,1)!=Compare3(false,1,1), что сделало бы функцию некорректной. То же самое для Compare4(). - person dshin; 02.04.2013
comment
Добавьте | (p1 == p2) и будет вам счастье. - person Griwes; 02.04.2013
comment
Хм, я не тестировал код. На моей домашней машине нет компилятора. Сейчас проверю. - person Sharath; 02.04.2013
comment
Блин, я пропустил это условие. Исправлено сейчас. Спасибо. - person Sharath; 02.04.2013
comment
Похоже, Compare4 самый быстрый. - person Sharath; 02.04.2013
comment
Собственно, по своим тестам я вижу, что Compare2 самый быстрый. Я получаю Compare2 примерно на 1,65 нс и Compare4 примерно на 2,55 нс. - person dshin; 02.04.2013
comment
На самом деле это не решает вопрос (т.е. почему разница между Compare() и Compare2()?) - person Oliver Charlesworth; 03.04.2013
comment
@OliCharlesworth это мягко сказано;) - person J.N.; 03.04.2013