Насколько эффективный, если оператор по сравнению с тестом, который не использует если? (C++)

Мне нужна программа для получения меньших из двух чисел, и я задаюсь вопросом при использовании стандарта, "если x является меньше, чем y"

int a, b, low;
if (a < b) low = a;
else low = b;

более или менее эффективно, чем это:

int a, b, low;
low = b + ((a - b) & ((a - b) >> 31));

(или изменение помещения int delta = a - b в вершине и rerplacing экземплярах a - b с этим).

Я просто задаюсь вопросом, какой из них был бы более эффективным (или если различие слишком миниатюрно, чтобы быть релевантным), и эффективность если еще операторы по сравнению с альтернативами в целом.

21
задан 25 February 2011 в 21:55
поделиться

13 ответов

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

Одним из преимуществ исключения оператора if является то, что вы избегаете штрафов за предсказание ветвления.

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

a = random() % 10
if (a < 5)
  print "Less"
else
  print "Greater"

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

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

Таким образом,

int a, b, low;
if (a < b) low = a;
else low = b;

становится

int a, b, low;
low = (a < b) ? a : b

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

Конечно, это микрооптимизация, которая вряд ли существенно повлияет на ваш код.

26
ответ дан 29 November 2019 в 06:29
поделиться

результаты профиля с gcc -o foo -g -p -O0, Solaris 9 v240

 %Time Seconds Cumsecs  #Calls   msec/call  Name
  36.8    0.21    0.21 8424829      0.0000  foo2
  28.1    0.16    0.37       1    160.      main
  17.5    0.10    0.4716850667      0.0000  _mcount
  17.5    0.10    0.57 8424829      0.0000  foo1
   0.0    0.00    0.57       4      0.      atexit
   0.0    0.00    0.57       1      0.      _fpsetsticky
   0.0    0.00    0.57       1      0.      _exithandle
   0.0    0.00    0.57       1      0.      _profil
   0.0    0.00    0.57    1000      0.000   rand
   0.0    0.00    0.57       1      0.      exit

код:

int
foo1 (int a, int b, int low)        
{
   if (a < b) 
      low = a;   
   else 
      low = b;         
   return low;
}

int                       
foo2 (int a, int b, int low)
{
   low = (a < b) ? a : b;
   return low;
}


int main()
{
    int low=0;
    int a=0;
    int b=0;
    int i=500;
    while (i--)
    {
       for(a=rand(), b=rand(); a; a--)
       {
           low=foo1(a,b,low);
           low=foo2(a,b,low);
       }        
    }
    return 0;
}

На основании данных в приведенной выше среде не было обнаружено, что полная противоположность нескольким утверждениям, изложенным здесь правда. Обратите внимание на «в этой среде». Если конструкция была быстрее, чем троичная? : construct

0
ответ дан 29 November 2019 в 06:29
поделиться

Если вы действительно не пытаетесь снизить эффективность, я не думаю, что вам стоит беспокоиться об этом.

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

-1
ответ дан 29 November 2019 в 06:29
поделиться

Самая большая проблема в том, что ваш второй пример не будет работать на 64-битных машинах .

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

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


[Edit] Поскольку я думаю, что это такая интересная тема, я написал на нее сообщение в блоге .

7
ответ дан 29 November 2019 в 06:29
поделиться

Если это для Gnu C ++,попробуйте это

int min = i <? j;

Я не профилировал его, но я думаю, что это определенно лучший вариант.

-1
ответ дан 29 November 2019 в 06:29
поделиться

Почему low = a; в if и low = a; в else? И, почему 31? Если 31 имеет отношение к размеру слова процессора, то что если код будет выполняться на процессоре другого размера?

Способ if..else выглядит более читабельным. Мне нравится, когда программы так же хорошо читаемы для человека, как и для компиляторов.

0
ответ дан 29 November 2019 в 06:29
поделиться

Мне просто интересно, какой из этих вариантов будет более эффективным (или если разница настолько мизерна, чтобы быть значимой), и эффективность операторов if-else по сравнению с альтернативами в целом.

Процессоры для настольных компьютеров и серверов оптимизированы для конвейеризации. Второе теоретически быстрее, потому что процессору не нужно ветвиться и он может использовать несколько АЛУ для параллельной оценки частей выражения. Для таких процессоров лучше всего подходит не разветвленный код с перемежающимися независимыми операциями. (Но даже это отрицается современными "условными" инструкциями CPU, которые позволяют сделать первый код без ветвления)

На встроенных CPU ветвление часто обходится дешевле (относительно всего остального), и у них нет большого количества свободных ALU для оценки операций вне порядка (это если они вообще поддерживают внепорядочное выполнение). Меньше кода/данных лучше - кэши тоже небольшие. (Я даже видел применение buble-sort во встроенных приложениях: алгоритм использует минимум памяти/кода и достаточно быстр для небольших объемов информации.)

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

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

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

1
ответ дан 29 November 2019 в 06:29
поделиться

Для чего-то столь простого, как это, почему бы просто не поэкспериментировать и не попробовать?

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

Я написал простую программу, которая сравнивает оба метода, передавая случайные числа (чтобы мы не видели идеального предсказания ветвей) с помощью Visual C++ 2010. Разница между подходами на моей машине для 100 000 000 итераций? Менее 50 мс, причем версия if оказалась быстрее. Если посмотреть на кодген, компилятор успешно преобразовал простой if в инструкцию cmovl, полностью избежав ветвления.

4
ответ дан 29 November 2019 в 06:29
поделиться

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

Я бы профилировал приложение и сосредоточил ваши усилия по оптимизации на чем-то более стоящем.

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

Я считаю тернарный оператор интуитивно понятным для таких простых операторов:

low = (a

Ясно и кратко.

4
ответ дан 29 November 2019 в 06:29
поделиться

Как и любую оптимизацию на низком уровне, протестируйте ее на целевом процессоре / настройке платы.

В моем компиляторе (gcc 4.5.1 на x86_64) первый пример становится

cmpl    %ebx, %eax
cmovle  %eax, %esi

Второй пример становится

subl    %eax, %ebx
movl    %ebx, %edx
sarl    $31, %edx
andl    %ebx, %edx
leal    (%rdx,%rax), %esi

Не уверен, что первый быстрее во всех случаях, но я уверен, что это так.

7
ответ дан 29 November 2019 в 06:29
поделиться

Компилируя это на gcc 4.3.4, amd64 (core 2 duo), Linux:

int foo1(int a, int b)
{
    int low;
    if (a < b) low = a;
    else low = b;
    return low;
}

int foo2(int a, int b)
{
    int low;
    low = b + ((a - b) & ((a - b) >> 31));
    return low;
}

я получаю:

foo1:
    cmpl    %edi, %esi
    cmovle  %esi, %edi
    movl    %edi, %eax
    ret

foo2:
    subl    %esi, %edi
    movl    %edi, %eax
    sarl    $31,  %eax
    andl    %edi, %eax
    addl    %esi, %eax
    ret

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

8
ответ дан 29 November 2019 в 06:29
поделиться

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

1
ответ дан 29 November 2019 в 06:29
поделиться

Простой ответ: Один условный переход будет эффективнее, чем два вычитания, одно сложение, побитовое и и операция сдвига вместе взятые. Меня достаточно учили по этому поводу (см. комментарии), так что я уже даже не уверен, что это обычно эффективнее.

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

9
ответ дан 29 November 2019 в 06:29
поделиться