Как лучше всего использовать оператор switch по сравнению с использованием оператора if для 30 перечислений unsigned, где около 10 имеют ожидаемое действие (в настоящее время это одно и то же действие). Необходимо учитывать производительность и пространство, но это не критично. Я абстрагировал фрагмент, так что не надо меня ненавидеть за соглашения об именах.
Заявление switch:
// numError is an error enumeration type, with 0 being the non-error case
// fire_special_event() is a stub method for the shared processing
switch (numError)
{
case ERROR_01 : // intentional fall-through
case ERROR_07 : // intentional fall-through
case ERROR_0A : // intentional fall-through
case ERROR_10 : // intentional fall-through
case ERROR_15 : // intentional fall-through
case ERROR_16 : // intentional fall-through
case ERROR_20 :
{
fire_special_event();
}
break;
default:
{
// error codes that require no additional action
}
break;
}
Заявление if:
if ((ERROR_01 == numError) ||
(ERROR_07 == numError) ||
(ERROR_0A == numError) ||
(ERROR_10 == numError) ||
(ERROR_15 == numError) ||
(ERROR_16 == numError) ||
(ERROR_20 == numError))
{
fire_special_event();
}
Я не согласен, не думаю, что это субъективно. Важна простая разница в ASM, во многих случаях нельзя просто игнорировать несколько секунд оптимизации. И в этом вопросе это не религиозная война или дебаты, есть рациональное объяснение того, почему нужно действовать быстрее, просто прочтите принятый ответ.
Что быстрее: stackoverflow.com/questions/6805026/is-switch-faster-than-if
@RichardFranks оффтоп: грц! ты первый человек, который взял на себя модерацию на ТАК, что я когда-либо видел





Компилятор все равно оптимизирует его - используйте переключатель, так как он наиболее читаемый.
Скорее всего, компилятор не коснется if-then-else. Собственно, gcc точно этого не сделает (на то есть веская причина). Clang оптимизирует оба случая в двоичный поиск. Например, см. это.
Свитч, хотя бы для удобочитаемости. На мой взгляд, гигантские операторы if труднее поддерживать и труднее читать.
ERROR_01: // умышленное провал
или же
(ERROR_01 == numError) ||
Последний вариант более подвержен ошибкам и требует большего набора текста и форматирования, чем первый.
Я не уверен в лучших практиках, но я бы использовал переключатель, а затем ловил намеренное падение с помощью `` по умолчанию ''
ИМО, это прекрасный пример того, для чего был сделан провал переключателя.
в C# это единственный случай, когда случаются упаднические мысли. Хороший аргумент прямо здесь.
Я бы выбрал оператор if для ясности и условности, хотя я уверен, что некоторые не согласятся. В конце концов, вы хотите сделать что-то if, какое-то условие выполняется! Переключатель с одним действием кажется немного ... ненужным.
Если ваши обращения, вероятно, останутся сгруппированными в будущем - если более одного случая соответствуют одному результату - переключение может оказаться более легким для чтения и обслуживания.
Я не тот человек, который расскажет вам о скорости и использовании памяти, но, глядя на статус коммутатора, намного проще понять, чем большой оператор if (особенно через 2-3 месяца)
Используйте переключатель.
В худшем случае компилятор сгенерирует тот же код, что и цепочка if-else, поэтому вы ничего не потеряете. Если вы сомневаетесь, сначала поместите наиболее распространенные случаи в оператор switch.
В лучшем случае оптимизатор может найти лучший способ сгенерировать код. Обычно компилятор строит двоичное дерево решений (сохраняет сравнения и выполняет переходы в среднем случае) или просто создает таблицу переходов (работает вообще без сравнений).
Технически все равно будет одно сравнение, чтобы убедиться, что значение перечисления находится в таблице переходов.
Jep. Это правда. Однако включение перечислений и обработка всех случаев может избавить от последнего сравнения.
Обратите внимание, что ряд ifs теоретически может быть проанализирован как то же самое, что и переключатель компилятора, но зачем рисковать? Используя переключатель, вы передаете именно то, что хотите, что упрощает генерацию кода.
jakoben: Это можно сделать, но только для цепочек if / else, подобных переключателям. На практике этого не происходит, потому что программисты используют переключатель. Я покопался в технологии компиляторов и поверьте мне: поиск таких «бесполезных» конструкций занимает много времени. Для разработчиков компиляторов такая оптимизация не имеет смысла.
@NilsPipenbrinck с легкостью построения псевдорекурсивных цепочек if-else в метапрограммировании шаблонов и сложностью генерации цепочек switchcase, это сопоставление может стать более важным. (и да, древний комментарий, но Интернет навсегда, или, по крайней мере, до следующего вторника)
Они работают одинаково хорошо. Производительность примерно такая же, как у современного компилятора.
Я предпочитаю операторы if операторам case, потому что они более удобочитаемы и более гибки - вы можете добавлять другие условия, не основанные на числовом равенстве, например «|| max <min». Но для простого случая, который вы разместили здесь, это не имеет особого значения, просто делайте то, что вам удобнее всего.
переключатель определенно предпочтительнее. Легче просмотреть список случаев переключателя и точно знать, что он делает, чем читать длинное условие if.
Дупликация в состоянии if бросается в глаза. Предположим, что на одном из == было написано !=; вы бы заметили? Или если один экземпляр numError был написан как nmuError, который просто скомпилировался?
Я обычно предпочитаю использовать полиморфизм вместо переключателя, но без дополнительных деталей контекста трудно сказать.
Что касается производительности, лучше всего использовать профилировщик для измерения производительности вашего приложения в условиях, аналогичных тем, которые вы ожидаете в реальных условиях. В противном случае вы, вероятно, оптимизируете не в том месте и не так.
Я бы сказал использовать SWITCH. Таким образом, вам нужно только добиться разных результатов. Ваши десять идентичных случаев могут использовать значение по умолчанию. Если вы измените все, что вам нужно, это явно реализовать изменение, нет необходимости редактировать значение по умолчанию. Также намного проще добавить или удалить регистры из SWITCH, чем редактировать IF и ELSEIF.
switch(numerror){
ERROR_20 : { fire_special_event(); } break;
default : { null; } break;
}
Возможно, даже проверьте свое состояние (в данном случае - числовую ошибку) по списку возможностей, возможно, к массиву, чтобы ваш ПЕРЕКЛЮЧАТЕЛЬ даже не использовался, если только не будет определенного результата.
Всего около 30 ошибок. 10 требуют специального действия, поэтому я использую значение по умолчанию для ~ 20 ошибок, которые не требуют действия ...
Я согласен с компактностью решения переключателя, но, IMO, вы здесь захват выключателя.
Назначение переключателя - обеспечить обработку разные в зависимости от значения.
Если бы вам пришлось объяснять свой алгоритм в псевдокоде, вы бы использовали if, потому что семантически это то, что он есть: если what_error сделать это ...
Поэтому, если вы когда-нибудь не собираетесь изменять свой код, чтобы иметь конкретный код для каждой ошибки, я бы использовал если.
Я не согласен по той же причине, по которой я не согласен со случаем провала. Я прочитал переключатель как «В случаях 01,07,0A, 10,15,16 и 20 пожарное особое событие». Здесь нет возможности перейти в другой раздел. Это всего лишь артефакт синтаксиса C++, в котором вы повторяете ключевое слово case для каждого значения.
Увидев, что у вас всего 30 кодов ошибок, создайте свою собственную таблицу переходов, а затем сделайте все варианты оптимизации самостоятельно (переход всегда будет самым быстрым), вместо того, чтобы надеяться, что компилятор поступит правильно. Это также делает код очень маленьким (кроме статического объявления таблицы переходов). У него также есть побочное преимущество, заключающееся в том, что с помощью отладчика вы можете изменять поведение во время выполнения, если вам это необходимо, просто путем прямого доступа к данным таблицы.
Вау, похоже, это способ превратить простую проблему в сложную. Зачем тратить столько усилий, если компилятор отлично за вас справится. Кроме того, очевидно, что это обработчик ошибок, поэтому скорость его работы вряд ли будет настолько критичной. Переключатель - это, безусловно, самая легкая вещь для чтения и обслуживания.
Таблица вряд ли сложна - на самом деле это, вероятно, проще, чем перейти к коду. И в заявлении упоминалось, что производительность была фактором.
Звучит как преждевременная оптимизация. Пока вы сохраняете свои значения enum маленькими и смежными, компилятор должен делать это за вас. Помещение переключателя в отдельную функцию сохраняет код, который его использует, красивым и маленьким, как предлагает Марк Рэнсом в своем ответе, дает такое же преимущество небольшого кода.
Также, если вы собираетесь что-то реализовать самостоятельно, сделайте std::bitset<MAXERR> specialerror;, а затем if (specialerror[err]) { special_handler(); }. Это будет быстрее, чем таблица прыжков, особенно. в не взятом случае.
Переключатель является быстрее.
Просто попробуйте if / else - введите 30 разных значений внутри цикла и сравните их с тем же кодом, используя переключатель, чтобы увидеть, насколько быстрее переключатель.
Теперь у коммутатора есть одна реальная проблема: переключатель должен знать во время компиляции значения внутри каждого случая. Это означает, что следующий код:
// WON'T COMPILE
extern const int MY_VALUE ;
void doSomething(const int p_iValue)
{
switch(p_iValue)
{
case MY_VALUE : /* do something */ ; break ;
default : /* do something else */ ; break ;
}
}
не компилируется.
Большинство людей затем будут использовать определения (ага!), А другие будут объявлять и определять постоянные переменные в той же единице компиляции. Например:
// WILL COMPILE
const int MY_VALUE = 25 ;
void doSomething(const int p_iValue)
{
switch(p_iValue)
{
case MY_VALUE : /* do something */ ; break ;
default : /* do something else */ ; break ;
}
}
Итак, в конце концов, разработчик должен выбрать между «скорость + ясность» или «кодовая связанность».
(Не то чтобы переключатель нельзя было записать так, чтобы он сбивал с толку ... Большинство переключателей, которые я сейчас вижу, относятся к этой "сбивающей с толку" категории ... Но это уже другая история ...)
Edit 2008-09-21:
bk1e added the following comment: "Defining constants as enums in a header file is another way to handle this".
Of course it is.
The point of an extern type was to decouple the value from the source. Defining this value as a macro, as a simple const int declaration, or even as an enum has the side-effect of inlining the value. Thus, should the define, the enum value, or the const int value change, a recompilation would be needed. The extern declaration means the there is no need to recompile in case of value change, but in the other hand, makes it impossible to use switch. The conclusion being Using switch will increase coupling between the switch code and the variables used as cases. When it is Ok, then use switch. When it isn't, then, no surprise.
.
Edit 2013-01-15:
Vlad Lazarenko commented on my answer, giving a link to his in-depth study of the assembly code generated by a switch. Very enlightning: http://lazarenko.me/switch/
Определение констант как перечислений в файле заголовка - еще один способ справиться с этим.
Переключатель - не всегда быстрее.
@Vlad Lazarenko: Спасибо за ссылку! Это было очень интересное чтение.
Ссылка @AhmedHussein user404725 мертва. К счастью, я нашел его в WayBack Machine: web.archive.org/web/20131111091431/http://lazarenko.me/2013/ 01 /…. Действительно, WayBack Machine может быть настоящим благословением.
Код для удобочитаемости. Если вы хотите знать, что работает лучше, используйте профилировщик, поскольку оптимизации и компиляторы различаются, а проблемы с производительностью редко возникают там, где люди думают.
Для особого случая, который вы указали в своем примере, наиболее четким кодом, вероятно, будет:
if (RequiresSpecialEvent(numError))
fire_special_event();
Очевидно, это просто перемещает проблему в другую область кода, но теперь у вас есть возможность повторно использовать этот тест. У вас также есть больше вариантов, как ее решить. Вы можете использовать std :: set, например:
bool RequiresSpecialEvent(int numError)
{
return specialSet.find(numError) != specialSet.end();
}
Я не утверждаю, что это лучшая реализация RequiresSpecialEvent, просто это вариант. Вы по-прежнему можете использовать переключатель или цепочку if-else, или таблицу поиска, или некоторые битовые манипуляции со значением, что угодно. Чем более непонятным становится ваш процесс принятия решений, тем больше пользы вы извлечете из его изолированной функции.
Это правда. Читаемость намного лучше, чем у switch и if-операторов. Я и сам собирался ответить примерно так, но вы меня опередили. :-)
Если все ваши значения перечисления маленькие, вам не нужен хеш, просто таблица. например const std::bitset<MAXERR> specialerror(initializer); Используйте его с if (specialerror[numError]) { fire_special_event(); }. Если вы хотите проверить границы, bitset::test(size_t) выдаст исключение для значений, выходящих за границы. (bitset::operator[] не проверяет диапазон). cplusplus.com/reference/bitset/bitset/test. Это, вероятно, превзойдет сгенерированную компилятором таблицу переходов, реализующую switch, особенно. в не-особом случае, когда это будет одна не занятая ветвь.
@PeterCordes Я все еще утверждаю, что лучше поместить таблицу в отдельную функцию. Как я уже сказал, при этом открывается лоты вариантов, я не пытался перечислить их все.
@MarkRansom: Я не хотел не соглашаться с абстрагированием. Как только вы дали пример реализации с использованием std::set, я подумал, что отмечу, что это, вероятно, плохой выбор. Оказывается, gcc уже компилирует код OP для немедленной проверки растрового изображения в 32-битном режиме. Godbolt: goo.gl/qjjv0e. gcc 5.2 сделает это даже для версии if. Кроме того, более поздний gcc будет использовать инструкцию битового теста bt вместо сдвига, чтобы поместить бит 1 в нужное место и использовать test reg, imm32.
Это растровое изображение с немедленной константой - большая победа, потому что растровое изображение не пропускает кэш. Он работает, если все "специальные" коды ошибок находятся в диапазоне 64 или меньше. (или 32 для устаревшего 32-битного кода.) Компилятор вычитает наименьшее значение case, если оно не равно нулю. Вывод состоит в том, что последние компиляторы достаточно умны, и вы, вероятно, получите хороший код из любой используемой логики, если только вы не укажете ему использовать громоздкую структуру данных.
С эстетической точки зрения я предпочитаю этот подход.
unsigned int special_events[] = {
ERROR_01,
ERROR_07,
ERROR_0A,
ERROR_10,
ERROR_15,
ERROR_16,
ERROR_20
};
int special_events_length = sizeof (special_events) / sizeof (unsigned int);
void process_event(unsigned int numError) {
for (int i = 0; i < special_events_length; i++) {
if (numError == special_events[i]) {
fire_special_event();
break;
}
}
}
Сделайте данные немного умнее, чтобы мы могли немного упростить логику.
Я понимаю, это выглядит странно. Вот источник вдохновения (из того, как я бы сделал это на Python):
special_events = [
ERROR_01,
ERROR_07,
ERROR_0A,
ERROR_10,
ERROR_15,
ERROR_16,
ERROR_20,
]
def process_event(numError):
if numError in special_events:
fire_special_event()
Синтаксис языка делает влияет на то, как мы реализуем решение ... => Это выглядит некрасиво на C и красиво на Python. :)
Использовать растровые изображения? Если error_0a равно 0x0a и т. д., Вы можете поместить их как биты в long long. long long special_events = 1LL << 1 | 1LL << 7 | 1LL << 0xa ... Затем используйте if (special_events & (1LL << numError) fire_special_event ()
Фу. Вы превратили операцию наихудшего случая O (1) (если сгенерированы таблицы переходов) в наихудший случай O (N) (где N - количество обработанных случаев), и вы использовали break вне case (да, небольшой грех, но тем не менее грех). :)
Фу? Он сказал, что производительность и пространство не критичны. Я просто предлагал другой взгляд на проблему. Если мы можем представить проблему таким образом, чтобы люди стали меньше думать, тогда меня обычно не волнует, означает ли это, что компьютеры должны думать больше.
Используйте переключатель, это то, для чего он нужен и чего ожидают программисты.
Я бы добавил лишние ярлыки на корпусе - просто чтобы люди чувствовали себя комфортно, я пытался вспомнить, когда и каковы правила, по которым их не нужно. Вы же не хотите, чтобы следующему программисту, работающему над этим, приходилось думать о деталях языка (возможно, через несколько месяцев это будете вы!)
Я знаю это старое, но
public class SwitchTest {
static final int max = 100000;
public static void main(String[] args) {
int counter1 = 0;
long start1 = 0l;
long total1 = 0l;
int counter2 = 0;
long start2 = 0l;
long total2 = 0l;
boolean loop = true;
start1 = System.currentTimeMillis();
while (true) {
if (counter1 == max) {
break;
} else {
counter1++;
}
}
total1 = System.currentTimeMillis() - start1;
start2 = System.currentTimeMillis();
while (loop) {
switch (counter2) {
case max:
loop = false;
break;
default:
counter2++;
}
}
total2 = System.currentTimeMillis() - start2;
System.out.println("While if/else: " + total1 + "ms");
System.out.println("Switch: " + total2 + "ms");
System.out.println("Max Loops: " + max);
System.exit(0);
}
}
Изменение количества циклов сильно меняет:
В то время как if / else: 5 мс Переключатель: 1 мс Максимальное количество петель: 100000
В то время как if / else: 5 мс Переключатель: 3 мс Максимальное количество петель: 1000000
В то время как if / else: 5 мс Переключатель: 14 мс Максимальное количество петель: 10000000
В то время как if / else: 5 мс Переключатель: 149 мс Максимальное количество петель: 100000000
(добавьте больше утверждений, если хотите)
Хорошее замечание, но извини, чувак, ты говоришь не на том языке. Изменение языка сильно меняет;)
Цикл if (max) break выполняется в постоянное время независимо от количества циклов? Похоже, JIT-компилятор достаточно умен, чтобы оптимизировать цикл до counter2=max. И, возможно, это медленнее, чем переключение, если первый вызов currentTimeMillis имеет больше накладных расходов, потому что еще не все JIT-скомпилировано? Размещение петель в другом порядке, вероятно, даст другие результаты.
while (true) != while (loop)
Вероятно, первый оптимизирован компилятором, что объясняет, почему второй цикл медленнее при увеличении количества циклов.
Похоже, это комментарий к ответу МакАникса. Это только одна из проблем, связанных с попыткой синхронизировать if и switch как условие завершения цикла в Java.
Компиляторы действительно хороши в оптимизации switch. Недавний gcc также хорош для оптимизации ряда условий в if.
Я сделал несколько тестовых примеров на Godbolt.
Когда значения case сгруппированы близко друг к другу, gcc, clang и icc достаточно умен, чтобы использовать растровое изображение, чтобы проверить, является ли значение одним из специальных.
например gcc 5.2 -O3 компилирует switch (и if что-то очень похожее):
errhandler_switch(errtype): # gcc 5.2 -O3
cmpl , %edi
ja .L5
movabsq 01325442, %rax # highest set bit is bit 32 (the 33rd bit)
btq %rdi, %rax
jc .L10
.L5:
rep ret
.L10:
jmp fire_special_event()
Обратите внимание, что растровое изображение - это немедленные данные, поэтому нет потенциального кэша данных, который может не получить доступ к нему, или к таблице переходов.
gcc 4.9.2 -O3 компилирует switch в растровое изображение, но выполняет 1U<<errNumber с помощью mov / shift. Он компилирует версию if в серию ответвлений.
errhandler_switch(errtype): # gcc 4.9.2 -O3
leal -1(%rdi), %ecx
cmpl , %ecx # cmpl , %edi wouldn't have to wait an extra cycle for lea's output.
# However, register read ports are limited on pre-SnB Intel
ja .L5
movl , %eax
salq %cl, %rax # with -march=haswell, it will use BMI's shlx to avoid moving the shift count into ecx
testl 50662721, %eax
jne .L10
.L5:
rep ret
.L10:
jmp fire_special_event()
Обратите внимание, как он вычитает 1 из errNumber (с lea, чтобы объединить эту операцию с перемещением). Это позволяет ему уместить растровое изображение в 32-битное сразу, избегая 64-битного немедленного movabsq, который требует больше байтов инструкций.
Более короткая (в машинном коде) последовательность будет:
cmpl , %edi
ja .L5
mov 50662721, %eax
dec %edi # movabsq and btq is fewer instructions / fewer Intel uops, but this saves several bytes
bt %edi, %eax
jc fire_special_event
.L5:
ret
(Отказ от использования jc fire_special_event повсеместен, и это ошибка компилятора.)
rep ret используется в целях перехода и следующих условных переходах в интересах старых AMD K8 и K10 (до Bulldozer): Что означает `rep ret`?. Без него предсказание ветвлений не работает на этих устаревших процессорах.
bt (битовый тест) с аргументом регистра выполняется быстро. Он сочетает в себе сдвиг влево единицы на бит errNumber и выполнение test, но по-прежнему имеет задержку в 1 цикл и только один uop Intel. Это медленно с аргументом памяти из-за его семантики слишком-слишком-CISC: с операндом памяти для «битовой строки» адрес проверяемого байта вычисляется на основе другого аргумента (деленного на 8) и isn Не ограничивается блоком размером 1, 2, 4 или 8 байтов, на который указывает операнд памяти.
Из Таблицы инструкций Агнера Фога инструкция сдвига с подсчетом переменных работает медленнее, чем bt на последних моделях Intel (2 мупа вместо 1, и shift не делает всего остального, что необходимо).
Что касается компиляции программы, я не знаю, есть ли разница. Но что касается самой программы и того, чтобы код был как можно более простым, я лично думаю, что это зависит от того, что вы хотите сделать. if else if else утверждения имеют свои преимущества, которые, как мне кажется, следующие:
позволяют тестировать переменную в определенных диапазонах вы можете использовать функции (стандартная библиотека или личные) как условные.
(пример:
`int a;
cout<<"enter value:\n";
cin>>a;
if ( a > 0 && a < 5)
{
cout<<"a is between 0, 5\n";
}else if (a > 5 && a < 10)
cout<<"a is between 5,10\n";
}else{
"a is not an integer, or is not in range 0,10\n";
Однако операторы If else if else могут стать сложными и беспорядочными (несмотря на все ваши попытки) в спешке. Операторы switch, как правило, яснее, понятнее и легче читаются; но может использоваться только для проверки определенных значений (пример:
`int a;
cout<<"enter value:\n";
cin>>a;
switch(a)
{
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
cout<<"a is between 0,5 and equals: "<<a<<"\n";
break;
//other case statements
default:
cout<<"a is not between the range or is not a good value\n"
break;
Я предпочитаю операторы if - else if - else, но это действительно зависит от вас. Если вы хотите использовать функции в качестве условий или хотите протестировать что-то в сравнении с диапазоном, массивом или вектором и / или вы не против иметь дело со сложным вложением, я бы рекомендовал использовать блоки If else if else. Если вы хотите протестировать отдельные значения или хотите чистый и легко читаемый блок, я бы порекомендовал вам использовать блоки case switch ().
Извините, что не согласен с текущим принятым ответом. Это 2021 год. Современные компиляторы и их оптимизаторы больше не должны делать различий между switch и эквивалентной цепочкой if. Если они все еще работают и создают плохо оптимизированный код для любого варианта, напишите поставщику компилятора (или сделайте его общедоступным здесь, что имеет более высокий уровень уважения), но не позволяйте микрооптимизациям влиять на ваш стиль кодирования.
Итак, если вы используете:
switch (numError) { case ERROR_A: case ERROR_B: ... }
или же:
if (numError == ERROR_A || numError == ERROR_B || ...) { ... }
или же:
template<typename C, typename EL>
bool has(const C& cont, const EL& el) {
return std::find(cont.begin(), cont.end(), el) != cont.end();
}
constexpr std::array errList = { ERROR_A, ERROR_B, ... };
if (has(errList, rnd)) { ... }
не должно влиять на скорость выполнения. Но в зависимости от того, над каким проектом вы работаете, они могут иметь большое значение в ясности и удобстве сопровождения кода. Например, если вам нужно проверить определенный список ошибок во многих местах кода, шаблонный has() может оказаться намного проще в обслуживании, поскольку errList необходимо обновлять только в одном месте.
Говоря о текущих компиляторах, я скомпилировал приведенный ниже тестовый код как с clang++ -O3 -std=c++1z (версии 10 и 11), так и с g++ -O3 -std=c++1z. Обе версии clang давали схожий скомпилированный код и время выполнения. Так что теперь я говорю только о версии 11. В частности, functionA() (который использует if) и functionB() (который использует switch) производят точно такой же вывод ассемблера с clang! И functionC() использует таблицу переходов, хотя многие другие плакаты считали таблицы переходов эксклюзивной особенностью switch. Однако, несмотря на то, что многие люди считают таблицы переходов оптимальными, на самом деле это было самое медленное решение для clang: functionC() требует примерно на 20 процентов больше времени выполнения, чем functionA() или functionB().
Версия functionH(), оптимизированная вручную, оказалась самой быстрой среди clang. Он даже частично развернул цикл, выполняя по две итерации в каждом цикле.
Фактически, clang вычисляет битовое поле, которое явно предоставляется в functionH(), а также в functionA() и functionB(). Однако он использовал условные переходы в functionA() и functionB(), что сделало их медленными, поскольку предсказание переходов регулярно дает сбой, в то время как он использовал гораздо более эффективный adc («добавить с переносом») в functionH(). Хотя он не смог применить эту очевидную оптимизацию и в других вариантах, мне неизвестно.
Код, созданный g++, выглядит намного сложнее, чем код clang, но на самом деле он работает немного быстрее для functionA() и намного быстрее для functionC(). Из всех функций, не оптимизированных вручную, functionC() является самым быстрым на g++ и быстрее, чем любая из функций на clang. Напротив, functionH() требует вдвое большего времени выполнения при компиляции с g++ вместо clang, в основном потому, что g++ не выполняет разворачивание цикла.
Вот подробные результаты:
clang:
functionA: 109877 3627
functionB: 109877 3626
functionC: 109877 4192
functionH: 109877 524
g++:
functionA: 109877 3337
functionB: 109877 4668
functionC: 109877 2890
functionH: 109877 982
Производительность кардинально меняется, если во всем коде константа 32 меняется на 63:
clang:
functionA: 106943 1435
functionB: 106943 1436
functionC: 106943 4191
functionH: 106943 524
g++:
functionA: 106943 1265
functionB: 106943 4481
functionC: 106943 2804
functionH: 106943 1038
Причина ускорения заключается в том, что в случае, если максимальное протестированное значение равно 63, компиляторы удаляют некоторые ненужные проверки привязки, потому что значение rnd в любом случае привязано к 63. Обратите внимание, что после удаления этой привязки неоптимизированный functionA(), использующий простой if () на g++, работает почти так же быстро, как functionH(), оптимизированный вручную, а также производит довольно похожий вывод на ассемблере.
Какой вывод? Если вы много вручную оптимизируете и тестируете компиляторы, вы получите самое быстрое решение. Любые предположения о том, что лучше - switch или if, недействительны - они одинаковы на clang. И простое в кодировании решение для проверки значений array на самом деле является самым быстрым случаем для g++ (если не учитывать ручную оптимизацию и сопоставлять последние значения в списке по случайным причинам).
Будущие версии компилятора будут все лучше и лучше оптимизировать ваш код и приблизиться к вашей ручной оптимизации. Так что не тратьте на это свое время, если только циклы ДЕЙСТВИТЕЛЬНО не важны в вашем случае.
Вот тестовый код:
#include <iostream>
#include <chrono>
#include <limits>
#include <array>
#include <algorithm>
unsigned long long functionA() {
unsigned long long cnt = 0;
for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
if (rnd == 1 || rnd == 7 || rnd == 10 || rnd == 16 ||
rnd == 21 || rnd == 22 || rnd == 63)
{
cnt += 1;
}
}
return cnt;
}
unsigned long long functionB() {
unsigned long long cnt = 0;
for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
switch(rnd) {
case 1:
case 7:
case 10:
case 16:
case 21:
case 22:
case 63:
cnt++;
break;
}
}
return cnt;
}
template<typename C, typename EL>
bool has(const C& cont, const EL& el) {
return std::find(cont.begin(), cont.end(), el) != cont.end();
}
unsigned long long functionC() {
unsigned long long cnt = 0;
constexpr std::array errList { 1, 7, 10, 16, 21, 22, 63 };
for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
cnt += has(errList, rnd);
}
return cnt;
}
// Hand optimized version (manually created bitfield):
unsigned long long functionH() {
unsigned long long cnt = 0;
const unsigned long long bitfield =
(1ULL << 1) +
(1ULL << 7) +
(1ULL << 10) +
(1ULL << 16) +
(1ULL << 21) +
(1ULL << 22) +
(1ULL << 63);
for(unsigned long long i = 0; i < 1000000; i++) {
unsigned char rnd = (((i * (i >> 3)) >> 8) ^ i) & 63;
if (bitfield & (1ULL << rnd)) {
cnt += 1;
}
}
return cnt;
}
void timeit(unsigned long long (*function)(), const char* message)
{
unsigned long long mintime = std::numeric_limits<unsigned long long>::max();
unsigned long long fres = 0;
for(int i = 0; i < 100; i++) {
auto t1 = std::chrono::high_resolution_clock::now();
fres = function();
auto t2 = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
if (duration < mintime) {
mintime = duration;
}
}
std::cout << message << fres << " " << mintime << std::endl;
}
int main(int argc, char* argv[]) {
timeit(functionA, "functionA: ");
timeit(functionB, "functionB: ");
timeit(functionC, "functionC: ");
timeit(functionH, "functionH: ");
timeit(functionA, "functionA: ");
timeit(functionB, "functionB: ");
timeit(functionC, "functionC: ");
timeit(functionH, "functionH: ");
timeit(functionA, "functionA: ");
timeit(functionB, "functionB: ");
timeit(functionC, "functionC: ");
timeit(functionH, "functionH: ");
return 0;
}
Конечно, вы можете увидеть это с точки зрения генерации наиболее эффективного кода, но любой современный компилятор должен быть столь же эффективным. В конце концов, это больше вопрос цвета навеса для велосипеда.