Мне нужна программа для получения меньших из двух чисел, и я задаюсь вопросом при использовании стандарта, "если 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
с этим).
Я просто задаюсь вопросом, какой из них был бы более эффективным (или если различие слишком миниатюрно, чтобы быть релевантным), и эффективность если еще операторы по сравнению с альтернативами в целом.
(Отказ от ответственности: нижеследующее касается очень низкоуровневых оптимизаций, которые чаще всего не нужны. Если вы продолжаете читать, вы отказываетесь от своего права жаловаться, что компьютеры работают быстро, и нет никаких причин беспокоиться о подобных вещах.)
Одним из преимуществ исключения оператора 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
, и во втором случае инструкция ветвления не требуется. Кроме того, он намного яснее и читабельнее, чем ваша реализация на основе битов.
Конечно, это микрооптимизация, которая вряд ли существенно повлияет на ваш код.
результаты профиля с 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
Если вы действительно не пытаетесь снизить эффективность, я не думаю, что вам стоит беспокоиться об этом.
Я просто подумал, что if будет быстрее, потому что он сравнивает одно, а другой код выполняет несколько операций. Но опять же, я полагаю, что разница мизерная.
Самая большая проблема в том, что ваш второй пример не будет работать на 64-битных машинах .
Однако, даже игнорируя это, современные компиляторы достаточно умны, чтобы рассматривать безотказное предсказание во всех возможных случаях и сравнивать оценочные скорости. Итак, ваш второй пример , скорее всего, на самом деле будет медленнее
. Не будет никакой разницы между оператором if и использованием тернарного оператора, поскольку даже большинство глупых компиляторов достаточно умны, чтобы распознать этот особый случай.
[Edit] Поскольку я думаю, что это такая интересная тема, я написал на нее сообщение в блоге .
Если это для Gnu C ++,попробуйте это
int min = i <? j;
Я не профилировал его, но я думаю, что это определенно лучший вариант.
Почему low = a;
в if
и low = a;
в else
? И, почему 31
? Если 31 имеет отношение к размеру слова процессора, то что если код будет выполняться на процессоре другого размера?
Способ if..else выглядит более читабельным. Мне нравится, когда программы так же хорошо читаемы для человека, как и для компиляторов.
Мне просто интересно, какой из этих вариантов будет более эффективным (или если разница настолько мизерна, чтобы быть значимой), и эффективность операторов if-else по сравнению с альтернативами в целом.
Процессоры для настольных компьютеров и серверов оптимизированы для конвейеризации. Второе теоретически быстрее, потому что процессору не нужно ветвиться и он может использовать несколько АЛУ для параллельной оценки частей выражения. Для таких процессоров лучше всего подходит не разветвленный код с перемежающимися независимыми операциями. (Но даже это отрицается современными "условными" инструкциями CPU, которые позволяют сделать первый код без ветвления)
На встроенных CPU ветвление часто обходится дешевле (относительно всего остального), и у них нет большого количества свободных ALU для оценки операций вне порядка (это если они вообще поддерживают внепорядочное выполнение). Меньше кода/данных лучше - кэши тоже небольшие. (Я даже видел применение buble-sort во встроенных приложениях: алгоритм использует минимум памяти/кода и достаточно быстр для небольших объемов информации.)
Важно: не забывайте об оптимизации компилятора. Используя множество трюков, компиляторы иногда сами могут убрать ветвление: инлайнинг, распространение констант, рефакторинг и т.д.
Но в итоге я бы сказал, что да, разница мизерная, чтобы быть значимой. В долгосрочной перспективе побеждает читабельный код.
Судя по тому, как обстоят дела на фронте процессоров, выгоднее вложить время сейчас в то, чтобы сделать код многопоточным и способным к OpenCL.
Для чего-то столь простого, как это, почему бы просто не поэкспериментировать и не попробовать?
Обычно сначала нужно составить профиль, определить, что это горячая точка, поэкспериментировать с изменениями и посмотреть результат.
Я написал простую программу, которая сравнивает оба метода, передавая случайные числа (чтобы мы не видели идеального предсказания ветвей) с помощью Visual C++ 2010. Разница между подходами на моей машине для 100 000 000 итераций? Менее 50 мс, причем версия if оказалась быстрее. Если посмотреть на кодген, компилятор успешно преобразовал простой if в инструкцию cmovl, полностью избежав ветвления.
В любом случае сборка будет состоять только из нескольких инструкций, и в любом случае выполнение этих инструкций займет пикосекунды.
Я бы профилировал приложение и сосредоточил ваши усилия по оптимизации на чем-то более стоящем.
Кроме того, время, сэкономленное благодаря этому типу оптимизации, не будет стоить времени, потраченного зря тем, кто пытается его поддерживать.
Я считаю тернарный оператор интуитивно понятным для таких простых операторов:
low = (a
Ясно и кратко.
Как и любую оптимизацию на низком уровне, протестируйте ее на целевом процессоре / настройке платы.
В моем компиляторе (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
Не уверен, что первый быстрее во всех случаях, но я уверен, что это так.
Компилируя это на 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 инструкции длиннее. Думаю, я продолжу кодирование и позволю компилятору делать свою работу.
Замечу одну вещь, которую я не заметил, упоминания о том, что подобная оптимизация легко может быть затруднена другими проблемами. Например, если вы запускаете эту процедуру на двух больших массивах чисел (или, что еще хуже, парах чисел, разбросанных в памяти), затраты на выборку значений на современных процессорах могут легко остановить конвейеры выполнения процессора.
Простой ответ: Один условный переход будет эффективнее, чем два вычитания, одно сложение, побитовое и и операция сдвига вместе взятые. Меня достаточно учили по этому поводу (см. комментарии), так что я уже даже не уверен, что это обычно эффективнее.
Прагматичный ответ: В любом случае, вы платите не столько за дополнительные циклы процессора, сколько за время, которое потребуется программисту, чтобы понять, что делает второй пример. Программируйте в первую очередь для удобочитаемости, во вторую - для эффективности.