Является ли switch
утверждение на самом деле быстрее, чем if
утверждение?
Я запустил код ниже на x64 C ++ компиляторе Visual Studio 2010 с /Ox
флагом:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define MAX_COUNT (1 << 29)
size_t counter = 0;
size_t testSwitch()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
switch (counter % 4 + 1)
{
case 1: counter += 4; break;
case 2: counter += 3; break;
case 3: counter += 2; break;
case 4: counter += 1; break;
}
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
size_t testIf()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
const size_t c = counter % 4 + 1;
if (c == 1) { counter += 4; }
else if (c == 2) { counter += 3; }
else if (c == 3) { counter += 2; }
else if (c == 4) { counter += 1; }
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
int main()
{
printf("Starting...\n");
printf("Switch statement: %u ms\n", testSwitch());
printf("If statement: %u ms\n", testIf());
}
и получил эти результаты:
Оператор переключения: 5261 мс
Если оператор: 5196 мс
Из того, что я узнал, switch
операторы, очевидно, используют таблицы переходов для оптимизации ветвления.
Вопросы:
Как будет выглядеть базовая таблица переходов в x86 или x64?
Этот код использует таблицу переходов?
Почему в этом примере нет разницы в производительности? Есть ли ситуация , в которой есть значительная разница в производительности?
Разборка кода:
testIf:
13FE81B10 sub rsp,48h
13FE81B14 call qword ptr [__imp_clock (13FE81128h)]
13FE81B1A mov dword ptr [start],eax
13FE81B1E mov qword ptr [i],0
13FE81B27 jmp testIf+26h (13FE81B36h)
13FE81B29 mov rax,qword ptr [i]
13FE81B2E inc rax
13FE81B31 mov qword ptr [i],rax
13FE81B36 cmp qword ptr [i],20000000h
13FE81B3F jae testIf+0C3h (13FE81BD3h)
13FE81B45 xor edx,edx
13FE81B47 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B4E mov ecx,4
13FE81B53 div rax,rcx
13FE81B56 mov rax,rdx
13FE81B59 inc rax
13FE81B5C mov qword ptr [c],rax
13FE81B61 cmp qword ptr [c],1
13FE81B67 jne testIf+6Dh (13FE81B7Dh)
13FE81B69 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B70 add rax,4
13FE81B74 mov qword ptr [counter (13FE835D0h)],rax
13FE81B7B jmp testIf+0BEh (13FE81BCEh)
13FE81B7D cmp qword ptr [c],2
13FE81B83 jne testIf+89h (13FE81B99h)
13FE81B85 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B8C add rax,3
13FE81B90 mov qword ptr [counter (13FE835D0h)],rax
13FE81B97 jmp testIf+0BEh (13FE81BCEh)
13FE81B99 cmp qword ptr [c],3
13FE81B9F jne testIf+0A5h (13FE81BB5h)
13FE81BA1 mov rax,qword ptr [counter (13FE835D0h)]
13FE81BA8 add rax,2
13FE81BAC mov qword ptr [counter (13FE835D0h)],rax
13FE81BB3 jmp testIf+0BEh (13FE81BCEh)
13FE81BB5 cmp qword ptr [c],4
13FE81BBB jne testIf+0BEh (13FE81BCEh)
13FE81BBD mov rax,qword ptr [counter (13FE835D0h)]
13FE81BC4 inc rax
13FE81BC7 mov qword ptr [counter (13FE835D0h)],rax
13FE81BCE jmp testIf+19h (13FE81B29h)
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)]
13FE81BD9 sub eax,dword ptr [start]
13FE81BDD imul eax,eax,3E8h
13FE81BE3 cdq
13FE81BE4 mov ecx,3E8h
13FE81BE9 idiv eax,ecx
13FE81BEB cdqe
13FE81BED add rsp,48h
13FE81BF1 ret
testSwitch:
13FE81C00 sub rsp,48h
13FE81C04 call qword ptr [__imp_clock (13FE81128h)]
13FE81C0A mov dword ptr [start],eax
13FE81C0E mov qword ptr [i],0
13FE81C17 jmp testSwitch+26h (13FE81C26h)
13FE81C19 mov rax,qword ptr [i]
13FE81C1E inc rax
13FE81C21 mov qword ptr [i],rax
13FE81C26 cmp qword ptr [i],20000000h
13FE81C2F jae testSwitch+0C5h (13FE81CC5h)
13FE81C35 xor edx,edx
13FE81C37 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C3E mov ecx,4
13FE81C43 div rax,rcx
13FE81C46 mov rax,rdx
13FE81C49 inc rax
13FE81C4C mov qword ptr [rsp+30h],rax
13FE81C51 cmp qword ptr [rsp+30h],1
13FE81C57 je testSwitch+73h (13FE81C73h)
13FE81C59 cmp qword ptr [rsp+30h],2
13FE81C5F je testSwitch+87h (13FE81C87h)
13FE81C61 cmp qword ptr [rsp+30h],3
13FE81C67 je testSwitch+9Bh (13FE81C9Bh)
13FE81C69 cmp qword ptr [rsp+30h],4
13FE81C6F je testSwitch+0AFh (13FE81CAFh)
13FE81C71 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C73 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C7A add rax,4
13FE81C7E mov qword ptr [counter (13FE835D0h)],rax
13FE81C85 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C87 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C8E add rax,3
13FE81C92 mov qword ptr [counter (13FE835D0h)],rax
13FE81C99 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C9B mov rax,qword ptr [counter (13FE835D0h)]
13FE81CA2 add rax,2
13FE81CA6 mov qword ptr [counter (13FE835D0h)],rax
13FE81CAD jmp testSwitch+0C0h (13FE81CC0h)
13FE81CAF mov rax,qword ptr [counter (13FE835D0h)]
13FE81CB6 inc rax
13FE81CB9 mov qword ptr [counter (13FE835D0h)],rax
13FE81CC0 jmp testSwitch+19h (13FE81C19h)
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)]
13FE81CCB sub eax,dword ptr [start]
13FE81CCF imul eax,eax,3E8h
13FE81CD5 cdq
13FE81CD6 mov ecx,3E8h
13FE81CDB idiv eax,ecx
13FE81CDD cdqe
13FE81CDF add rsp,48h
13FE81CE3 ret
Обновить:
Интересные результаты здесь . Не уверен, почему один быстрее, а другой медленнее.
c
performance
switch-statement
assembly
jump-table
user541686
источник
источник
5196 vs. 5261 shouldn't be enough to actually care
-> Я не уверен, неправильно ли вы поняли вопрос или я неправильно понял ваш комментарий, но разве не весь смысл моего вопроса в том, почему нет разницы? (Я когда-либо утверждал, что это значительная разница, о которой нужно заботиться?)Ответы:
Есть несколько оптимизаций, которые компилятор может сделать на коммутаторе. Я не думаю, что часто упоминаемая «таблица переходов» является очень полезной, поскольку она работает только тогда, когда ввод может быть каким-то образом ограничен.
C псевдокодом для «таблицы переходов» будет что-то вроде этого - обратите внимание, что на практике компилятору потребуется вставить некоторую форму проверки if вокруг таблицы, чтобы убедиться, что входные данные были допустимы в таблице. Также обратите внимание, что это работает только в конкретном случае, когда ввод представляет собой последовательность последовательных чисел.
Если число ветвлений в коммутаторе очень велико, компилятор может делать что-то вроде использования бинарного поиска по значениям коммутатора, что (на мой взгляд) было бы гораздо более полезной оптимизацией, поскольку в некоторых случаях это значительно повышает производительность сценарии, такие же общие, как и переключатель, и не приводят к большему размеру сгенерированного кода. Но чтобы увидеть это, вашему тестовому коду понадобится МНОГО больше веток, чтобы увидеть разницу.
Чтобы ответить на ваши конкретные вопросы:
Clang генерирует тот, который выглядит следующим образом :
Я могу сказать, что он не использует таблицу переходов - ясно видно 4 инструкции сравнения:
Решение на основе таблицы переходов вообще не использует сравнение.
РЕДАКТИРОВАТЬ 2014 : В другом месте обсуждались люди, знакомые с оптимизатором LLVM, которые говорили, что оптимизация таблицы переходов может быть важной во многих сценариях; например, в случаях, когда существует перечисление со многими значениями и во многих случаях со значениями в указанном перечислении. Тем не менее, я придерживаюсь того, что я сказал выше в 2011 году - слишком часто я вижу, что люди думают: «Если я сделаю это переключение, это будет то же самое время, независимо от того, сколько у меня дел» - и это совершенно неверно. Даже с таблицей переходов вы получаете косвенную стоимость перехода и платите за записи в таблице для каждого случая; и пропускная способность памяти - это большое дело для современного оборудования.
Напишите код для удобства чтения. Любой компилятор, достойный своей соли, увидит лестницу if / else if и преобразует ее в эквивалентный переключатель, или наоборот, если это будет быстрее.
источник
switch
выходы. Сорен сказал несколько других вещей, которые я хотел сказать после прочтения этого ответа.if
предложений уже был настроен вручную, чтобы соответствовать частоте и относительным потребностям производительности, гдеswitch
традиционно рассматривается как открытое приглашение для оптимизации, как компилятор выбирает. Хороший момент, повторяющий прошлоеswitch
:-). Размер кода зависит от случаев / диапазона - может быть лучше. Наконец, некоторые перечисления, битовые поля иchar
сценарии по своей природе действительны / ограничены и не требуют дополнительных затрат.На ваш вопрос:
1. Как будет выглядеть базовая таблица переходов в x86 или x64?
Таблица переходов - это адрес памяти, который содержит указатель на метки в виде структуры массива. Следующий пример поможет вам понять, как устроены таблицы переходов
Где 00B14538 - указатель на таблицу переходов и значение, например, D8 09 AB 00 представляет указатель метки.
2. Использует ли этот код таблицу переходов? Нет в этом случае.
3. Почему в этом примере нет разницы в производительности?
Нет разницы в производительности, потому что инструкция для обоих случаев выглядит одинаково, нет таблицы переходов.
4. Есть ли ситуации, в которых есть существенная разница в производительности?
Если у вас очень длинная последовательность проверки if , в этом случае использование таблицы переходов повышает производительность (инструкции ветвления / jmp дороги если они не предсказывают почти идеально), но требуют затрат памяти.
Код для всех инструкций сравнения также имеет некоторый размер, поэтому, особенно при использовании 32-битных указателей или смещений, поиск в одной таблице переходов может не стоить намного большего размера в исполняемом файле.
Вывод: компилятор достаточно умен, обрабатывает такие случаи и генерирует соответствующие инструкции :)
источник
gcc -S
вывод: последовательность записей.long L1
/.long L2
таблицы более значима, чем hexdump, и более полезна для тех, кто хочет научиться смотреть на компилятор. (Хотя я думаю, что вы просто посмотрите на код коммутатора, чтобы увидеть, был ли это косвенный jmp или связка jcc).Компилятор может свободно скомпилировать оператор switch в виде кода, который эквивалентен оператору if, или создать таблицу переходов. Скорее всего, он будет выбирать один из другого в зависимости от того, что будет выполняться быстрее всего или генерировать наименьший код, в зависимости от того, что вы указали в параметрах компилятора, поэтому в худшем случае скорость будет такой же, как в операторах if.
Я бы доверил компилятору сделать лучший выбор и сосредоточился на том, что делает код наиболее читабельным.
Если число случаев становится очень большим, таблица переходов будет намного быстрее, чем серия if. Однако, если шаги между значениями очень велики, тогда таблица переходов может стать большой, и компилятор может не генерировать ее.
источник
Откуда вы знаете, что ваш компьютер не выполнял какую-либо задачу, не связанную с тестом, во время цикла тестирования коммутатора и выполнял меньше задач во время цикла тестирования if? Результаты вашего теста ничего не показывают как:
Мои результаты:
Я добавил:
до конца, чтобы не оптимизировать цикл, так как в вашем примере счетчик никогда не использовался, так почему компилятор выполняет цикл? Сразу же, переключатель всегда выигрывал даже с таким микро-тестом.
Другая проблема с вашим кодом:
в вашем цикле переключения, по сравнению с
в вашем цикле if. Очень большая разница, если вы это исправите. Я считаю, что размещение оператора внутри оператора switch заставляет компилятор отправлять значение непосредственно в регистры ЦП, а не помещать его в стек первым. Следовательно, это в пользу оператора switch, а не сбалансированного теста.
Ох, и я думаю, что вы также должны сбросить счетчик между тестами. Фактически, вы, вероятно, должны использовать какое-то случайное число вместо +1, +2, +3 и т. Д., Так как оно, вероятно, что-то там оптимизирует. Например, под случайным числом подразумевается число, основанное на текущем времени. В противном случае компилятор может превратить обе ваши функции в одну длинную математическую операцию и даже не беспокоиться ни о каких циклах.
Я изменил код Райана настолько, чтобы компилятор не мог разобраться до запуска кода:
переключатель: 3740
если: 3980
(аналогичные результаты для нескольких попыток)
Я также уменьшил количество дел / случаев до 5, и функция переключения все еще выиграла.
источник
print
утверждение? Я добавил его в конце всей программы и не увидел никакой разницы. Я также не понимаю, в чем «проблема» с другим ... умом объяснять, что такое «очень большая разница»?Хороший оптимизирующий компилятор, такой как MSVC, может генерировать:
Короче говоря, если переключатель выглядит медленнее, чем серия ifs, компилятор может просто преобразовать его в единицу. И это, вероятно, будет не просто последовательность сравнений для каждого случая, а двоичное дерево поиска. Смотрите здесь для примера.
источник
Я отвечу 2) и сделаю некоторые общие комментарии. 2) Нет, в коде сборки, который вы опубликовали, нет таблицы переходов. Таблица переходов - это таблица пунктов назначения перехода и одна или две инструкции для перехода непосредственно в индексированное местоположение из таблицы. Таблица переходов имеет больше смысла, когда есть много возможных пунктов назначения переключения. Может быть, оптимизатор знает, что простая, если еще логика быстрее, если количество пунктов назначения не превышает некоторый порог. Попробуйте еще раз, скажем, 20 вариантов вместо 4.
источник
Я был заинтригован и посмотрел, что я могу изменить в вашем примере, чтобы он быстрее выполнял оператор switch.
Если вы наберете 40 операторов if и добавите 0, то блок if будет работать медленнее, чем эквивалентный оператор switch. У меня есть результаты здесь: https://www.ideone.com/KZeCz .
Эффект удаления случая 0 можно увидеть здесь: https://www.ideone.com/LFnrX .
источник
Вот некоторые результаты из старого (сейчас трудно найти) теста Bench ++:
Из этого видно, что (на этой машине с этим компилятором - VC ++ 9.0 x64) каждый
if
тест занимает около 0,7 наносекунд. По мере увеличения количества тестов время масштабируется почти идеально линейно.С оператором switch почти нет разницы в скорости между 2-х и 10-ти сторонним тестом, если значения плотные. Тест с 10 путями с разреженными значениями занимает примерно в 1,6 раза больше времени, чем тест с 10 путями с плотными значениями, но даже при разреженных значениях он все же лучше, чем в два раза быстрее, чем скорость с 10 путями
if
/else if
.Итог: с использованием только тест 4-полосная не будет на самом деле показать вам много о производительности
switch
противif
/else
. Если вы посмотрите на числа из этого кода, то довольно просто интерполировать тот факт, что для теста с четырьмя путями мы ожидаем, что эти два приведут к довольно схожим результатам (~ 2,8 наносекунды дляif
/else
, ~ 2,0 дляswitch
).источник
if
/else
цепочки, против их разброса и т. Д. Не удается найтиbench++
источники после 10 минут гуглят.Обратите внимание, что когда переключатель НЕ компилируется в таблицу переходов, вы можете очень часто писать, если он более эффективен, чем переключатель ...
(1) если дела имеют порядок, а не тестирование наихудшего случая для всех N, вы можете написать свои if для проверки, в верхней или нижней половине, а затем в каждой половине этого стиля двоичного поиска ... что приводит к в худшем случае это logN, а не N
(2) если определенные случаи / группы встречаются гораздо чаще, чем другие, то разработка вашего if для первичного выделения этих случаев может ускорить среднее время
источник
Нет, это если если затем перейти к другому, если затем перейти к другому ... Таблица перехода будет иметь таблицу адресов или использовать хэш или что-то в этом роде.
Быстрее или медленнее субъективно. Например, вы могли бы использовать случай 1 последним, а не первым, и если ваша тестовая программа или программа реального мира использовала случай 1, большую часть времени код был бы медленнее в этой реализации. Так что просто реорганизация списка дел, в зависимости от реализации, может иметь большое значение.
Если бы вы использовали случаи 0-3 вместо 1-4, компилятор мог бы использовать таблицу переходов, компилятор должен был бы в любом случае удалить ваш +1. Возможно, это было небольшое количество предметов. Например, если бы вы сделали это 0 - 15 или 0 - 31, он мог бы реализовать это с помощью таблицы или использовать другой ярлык. Компилятор может свободно выбирать, как он реализует вещи, если он соответствует функциональности исходного кода. И это касается различий компилятора, различий версий и различий в оптимизации. Если вам нужна таблица переходов, создайте таблицу переходов, если вы хотите, чтобы дерево if-then-else создавало дерево if-then-else. Если вы хотите, чтобы компилятор принял решение, используйте оператор switch / case.
источник
Это на самом деле не так уж сложно объяснить ... Если вы помните, что неправильно предсказанные ветки стоят в десятки и сотни раз дороже, чем правильно предсказанные ветки.
в
% 20
версии первый случай / если всегда тот, который поражает. Современные процессоры «узнают», какие ветви обычно используются, а какие нет, поэтому они могут легко предсказать, как эта ветвь будет вести себя практически на каждой итерации цикла. Это объясняет, почему вылетает версия «если»; он никогда не должен выполнять что-либо после первого теста, и он (правильно) предсказывает результат этого теста для большинства итераций. Очевидно, что «переключатель» реализован несколько иначе - возможно, даже таблица переходов, которая может быть медленной благодаря вычисляемой ветви.в
% 21
версии ветки по сути случайные. Так что не только многие из них выполняют каждую итерацию, процессор не может угадать, в каком направлении они пойдут. Это тот случай, когда таблица переходов (или другая оптимизация «переключения») может помочь.Очень сложно предсказать, как часть кода будет работать с современным компилятором и процессором, и это становится сложнее с каждым поколением. Лучший совет - «даже не пытайся, всегда в профиле». Этот совет становится лучше - и количество людей, которые могут его игнорировать, с каждым годом становится все меньше.
Все это говорит о том, что мое объяснение, приведенное выше, в значительной степени является предположением. :-)
источник
Никто. В большинстве случаев, когда вы переходите на ассемблер и выполняете реальные измерения производительности, ваш вопрос просто не тот. Для данного примера ваше мышление становится слишком коротким, поскольку
выглядит для меня как правильное выражение приращения, которое вы должны использовать.
источник