В третьей статье мы продолжим разбирать условия. В прошлый раз мы так и не посмотрели оптимизированные версии if-else.
Возможно, из-за этого и возникли справедливые вопросы, например, почему компилятор вот тут:
сгенерировал код с двумя переходами, а не с одним.
Действительно, мы тут и сами немного странно написали код, ведь, очевидно, что его можно было написать и так:
Следовало бы спрашивать у программиста, зачем ему else, а не у компилятора. Но если мы посмотрим на оптимизированную версию первого варианта, то увидим, что компилятор не так глуп:
Сразу стоит заметить, что переходов здесь вообще нет. А способ, которым разобрался clang с нашим условием, мы уже видели. Для второго варианта будет сгенерирован точно такой же оптимизированный код.
Теперь давайте вспомним про это:
Было немного страшно в прошлый раз? Оптимизированная версия может несколько сократить объем кода и количество меток (я пропущу scanf):
Вплоть до метки .LBB0_2 код соответствует самому условию. Обратите внимание, что и в оптимизированной версии логика: «второй операнд «или» рассчитываться не будет, если первый — истина» не нарушается. Ну еще бы, она нарушалась…
Однако здесь есть существенное отличие от неоптимизированной версии. Давайте пройдемся по коду, чтобы все поняли, что происходит:
В первой строке в ecx попадает значение переменной «а».
Во второй строке в eax попадает а + 1
Далее идет сравнение с нулем, чтобы инструкцией js перейти на метку с номером 2, если число было отрицательным.
В четвертой строке в переменную «а» попадает а + 1 из eax.
В неоптимизированной версии мы брали снова значение переменной и помещали его в eax, но сейчас, мы берем старое значение, оно до сих пор в ecx. Далее мы делаем а + 2 и сохраняем в eax.
А сравниваем число до инкремента с 5, чтобы инструкцией jl, перейти к третьей метке, если значение меньше, тем самым, не попадать в условие.
У вас должен возникнуть справед��ивый вопрос: «Что же clang выдаст два разных результата для оптимизированной и неоптимизированной версии программы?»
Нет, штука в том, что уход в else происходит при меньше, чем 5, а не при меньше либо равно. Компилятор учел сомнительный результат, когда переменная равнялась бы 5, и построил условие. В итоге сэкономил на дополнительном обращении к памяти.
В оптимизированной версии:
5 >= 5 => 5 + 3
В неоптимизированной
7 > 5 => 7 + 1
Иначе в оптимизированной версии просто можно добавить 4 к результату и закинуть его в eax. Вот такие чудеса.
Почему я сравниваю неоптимизированную версию gcc с оптимизированную clang? Ну, gcc делает тоже самое, только не очень красиво. А в clang код выглядит последовательным и компактным.
Нужно упомянуть, что компилятор в VS2015 на полной оптимизации оставил всего одну метку:
В промежутках код, который не относится к нашему, так что я просто убрал его. Как можете заметить, условия построены так, чтобы пропустить else, поэтому только одна метка. Но стоит добавить код после else, и метки также будет две. При этом код будет напоминать неоптимизированную версию тем фактом, что сначала мы присвоим одно значение, но если выполниться условие, то значение перепишется.
Ладно, а что там с оператором switch? Чем батарея if-ов будет отличаться от этого «красивого» оператора. Давайте выясним на примере следующего кода:
и его альтернативе:
Мы не будем рассматривать подробно неоптимизированную версию. Я думаю все и так понимают, что различия если и будут, то минимальны. Но забавно, что именно для switch в итоге генерируется больше кода (gcc 7.2):
В случае с if, вы уже должны представлять код: сравнение — действие — переход. А здесь вы видите, что действия откладываются на потом.
В оптимизированной версии gcc для switch сгенерирует код короче, а в clang разницы вообще не будет. Поэтому давайте рассмотрим только switch в gcc. Но прежде подумайте, как вы бы могли вообще убрать switch в данном коде?
Итак, если придумали, то раскрываем спойлер и смотрим:
Для 0 ('e') результат будет 2,
Для 12 ('q') результат будет 0,
Для 18 ('w') результат будет 1,
В остальных случаях будет 4.
У нас сильно оторванный пример от жизни, вряд ли, так кто-то, вообще, switch использовал. Что если идут какие-то действия? Возможно, вы удивитесь, но в целом ничего не меняется. Рассмотрим такой пример:
Кода много, поэтому сконцентрируйтесь на главном. Сейчас, вместо перемещения числа в eax, осуществляется переход на определенную метку. Но сама идея не меняется.
Кажется, с условиями мы разобрались окончательно. Если у кого-то были сомнения насчет switch, то можно теперь его не бояться. В следующей статье мы посмотрим на циклы.
Предыдущая статья
Возможно, из-за этого и возникли справедливые вопросы, например, почему компилятор вот тут:
int x = 10; scanf("%i", &x); int c = 0; if (x < 4) { c = 3; } else { c = 2; } return c;
сгенерировал код с двумя переходами, а не с одним.
Действительно, мы тут и сами немного странно написали код, ведь, очевидно, что его можно было написать и так:
int x = 10; scanf("%i", &x); int c = 2; if (x < 4) { c = 3; } return c;
Следовало бы спрашивать у программиста, зачем ему else, а не у компилятора. Но если мы посмотрим на оптимизированную версию первого варианта, то увидим, что компилятор не так глуп:
sub esp, 12 mov dword ptr [esp + 8], 10 sub esp, 8 lea eax, [esp + 16] push eax push .L.str call scanf add esp, 16 xor eax, eax ; обнуление регистра eax cmp dword ptr [esp + 8], 4 ; сравниваем переменную и число 4 setl al ; если переменная меньше, то выставляем al в 1 or eax, 2 ; добавляем к результату 2 add esp, 12 ret .L.str: .asciz "%i"
Сразу стоит заметить, что переходов здесь вообще нет. А способ, которым разобрался clang с нашим условием, мы уже видели. Для второго варианта будет сгенерирован точно такой же оптимизированный код.
Теперь давайте вспомним про это:
int main(void) { int a = -1; scanf("%i", &a); if (a++ < 0 || a++ > 5) { a++; } else { a+=2; } return a; }
Было немного страшно в прошлый раз? Оптимизированная версия может несколько сократить объем кода и количество меток (я пропущу scanf):
mov ecx, dword ptr [esp + 8] lea eax, [ecx + 1] test ecx, ecx mov dword ptr [esp + 8], eax js .LBB0_2 lea eax, [ecx + 2] cmp ecx, 5 mov dword ptr [esp + 8], eax jl .LBB0_3 .LBB0_2: inc eax add esp, 12 ret .LBB0_3: add ecx, 4 mov eax, ecx add esp, 12 ret
Вплоть до метки .LBB0_2 код соответствует самому условию. Обратите внимание, что и в оптимизированной версии логика: «второй операнд «или» рассчитываться не будет, если первый — истина» не нарушается. Ну еще бы, она нарушалась…
Однако здесь есть существенное отличие от неоптимизированной версии. Давайте пройдемся по коду, чтобы все поняли, что происходит:
В первой строке в ecx попадает значение переменной «а».
Во второй строке в eax попадает а + 1
Далее идет сравнение с нулем, чтобы инструкцией js перейти на метку с номером 2, если число было отрицательным.
В четвертой строке в переменную «а» попадает а + 1 из eax.
В неоптимизированной версии мы брали снова значение переменной и помещали его в eax, но сейчас, мы берем старое значение, оно до сих пор в ecx. Далее мы делаем а + 2 и сохраняем в eax.
А сравниваем число до инкремента с 5, чтобы инструкцией jl, перейти к третьей метке, если значение меньше, тем самым, не попадать в условие.
У вас должен возникнуть справед��ивый вопрос: «Что же clang выдаст два разных результата для оптимизированной и неоптимизированной версии программы?»
Нет, штука в том, что уход в else происходит при меньше, чем 5, а не при меньше либо равно. Компилятор учел сомнительный результат, когда переменная равнялась бы 5, и построил условие. В итоге сэкономил на дополнительном обращении к памяти.
В оптимизированной версии:
5 >= 5 => 5 + 3
В неоптимизированной
7 > 5 => 7 + 1
Иначе в оптимизированной версии просто можно добавить 4 к результату и закинуть его в eax. Вот такие чудеса.
Почему я сравниваю неоптимизированную версию gcc с оптимизированную clang? Ну, gcc делает тоже самое, только не очень красиво. А в clang код выглядит последовательным и компактным.
Нужно упомянуть, что компилятор в VS2015 на полной оптимизации оставил всего одну метку:
mov eax, DWORD PTR _a$[ebp] ;a → eax add esp, 8 mov ecx, eax inc eax test ecx, ecx js SHORT $LN4@main ; if (a<0) mov ecx, eax inc eax cmp ecx, 5 jg SHORT $LN4@main ; if (a>5) add eax, 2 ;else a+=2 ... $LN4@main: ... inc eax ;a++ in if ...
В промежутках код, который не относится к нашему, так что я просто убрал его. Как можете заметить, условия построены так, чтобы пропустить else, поэтому только одна метка. Но стоит добавить код после else, и метки также будет две. При этом код будет напоминать неоптимизированную версию тем фактом, что сначала мы присвоим одно значение, но если выполниться условие, то значение перепишется.
switch
Ладно, а что там с оператором switch? Чем батарея if-ов будет отличаться от этого «красивого» оператора. Давайте выясним на примере следующего кода:
char c = getchar(); int a; if (c == 'q') { a = 0; } else if (c == 'w') { a = 1; } else if (c == 'e') { a = 2; } else { a = 4; } return a;
и его альтернативе:
char c = getchar(); int a; switch(c) { case 'q': a = 0; break; case 'w': a = 1; break; case 'e': a = 2; break; default: a = 4; } return a;
Мы не будем рассматривать подробно неоптимизированную версию. Я думаю все и так понимают, что различия если и будут, то минимальны. Но забавно, что именно для switch в итоге генерируется больше кода (gcc 7.2):
movsx eax, BYTE PTR [ebp-13] ;преобразует операнд со знаком в эквивалентный ему операнд со знаком большей размерности cmp eax, 113 je .L3 cmp eax, 119 je .L4 cmp eax, 101 je .L5 jmp .L8
В случае с if, вы уже должны представлять код: сравнение — действие — переход. А здесь вы видите, что действия откладываются на потом.
В оптимизированной версии gcc для switch сгенерирует код короче, а в clang разницы вообще не будет. Поэтому давайте рассмотрим только switch в gcc. Но прежде подумайте, как вы бы могли вообще убрать switch в данном коде?
Итак, если придумали, то раскрываем спойлер и смотрим:
asm код
Идея очень проста: сопоставить код символа и итоговое число в «а». Этого можно добиться тем, что мы отнимаем от результата наименьший код символа из switch, в нашем случае — это 'e' или 101.
Затем мы помещаем в eax 4, потому что по else у нас будет именно это.
Далее мы сравниваем содержимое edx и 18 (только берем младший байт из этого регистра, наподобие al, только для edx – это dl). И если значение больше (ja), то переходим к метке .L1. Если возник вопрос: «почему 18?», то выполните 'w' – 101.
Теперь, если результат лежит между 0 и 18, то работает формула: mov eax, DWORD PTR CSWTCH.3[0+edx*4], что означает просто взять результат, на основе числа в edx, смещение 4 отделяет одно число от другого в памяти (4 байта).
push DWORD PTR stdin call _IO_getc lea edx, [eax-101] add esp, 16 mov eax, 4 cmp dl, 18 ja .L1 movzx edx, dl mov eax, DWORD PTR CSWTCH.3[0+edx*4] .L1: mov ecx, DWORD PTR [ebp-4] leave lea esp, [ecx-4] ret CSWTCH.3: .long 2 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 4 .long 0 .long 4 .long 4 .long 4 .long 4 .long 4 .long 1
Идея очень проста: сопоставить код символа и итоговое число в «а». Этого можно добиться тем, что мы отнимаем от результата наименьший код символа из switch, в нашем случае — это 'e' или 101.
Затем мы помещаем в eax 4, потому что по else у нас будет именно это.
Далее мы сравниваем содержимое edx и 18 (только берем младший байт из этого регистра, наподобие al, только для edx – это dl). И если значение больше (ja), то переходим к метке .L1. Если возник вопрос: «почему 18?», то выполните 'w' – 101.
Теперь, если результат лежит между 0 и 18, то работает формула: mov eax, DWORD PTR CSWTCH.3[0+edx*4], что означает просто взять результат, на основе числа в edx, смещение 4 отделяет одно число от другого в памяти (4 байта).
Для 0 ('e') результат будет 2,
Для 12 ('q') результат будет 0,
Для 18 ('w') результат будет 1,
В остальных случаях будет 4.
У нас сильно оторванный пример от жизни, вряд ли, так кто-то, вообще, switch использовал. Что если идут какие-то действия? Возможно, вы удивитесь, но в целом ничего не меняется. Рассмотрим такой пример:
#include <stdio.h> #include <math.h> int main(void) { start: char c = getchar(); switch (c) { case 'T': case 't': printf("Talk\n"); break; case 'W': case 'w': printf("%f\n", sin(0)); break; case 'Q': case 'q': return 0; default: printf("wrong command\n"); } goto start; }
asm код
.LC0: .string "Talk" .LC2: .string "%f\n" .LC3: .string "wrong command" main: ... .L2: ;start sub esp, 12 push DWORD PTR stdin call _IO_getc sub eax, 81 add esp, 16 cmp al, 38 ja .L3 movzx eax, al jmp [DWORD PTR .L5[0+eax*4]] .L5: .long .L9 .long .L3 .long .L3 .long .L6 .long .L3 .long .L3 .long .L7 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L3 .long .L9 .long .L3 .long .L3 .long .L6 .long .L3 .long .L3 .long .L7 .L7: ;sin sub esp, 4 push 0 push 0 push OFFSET FLAT:.LC2 call printf add esp, 16 jmp .L2 .L6: ;Talk sub esp, 12 push OFFSET FLAT:.LC0 call puts add esp, 16 jmp .L2 .L9: ;return 0; mov ecx, DWORD PTR [ebp-4] xor eax, eax leave lea esp, [ecx-4] ret .L3: ;default sub esp, 12 push OFFSET FLAT:.LC3 call puts add esp, 16 jmp .L2
Кода много, поэтому сконцентрируйтесь на главном. Сейчас, вместо перемещения числа в eax, осуществляется переход на определенную метку. Но сама идея не меняется.
Заключение
Кажется, с условиями мы разобрались окончательно. Если у кого-то были сомнения насчет switch, то можно теперь его не бояться. В следующей статье мы посмотрим на циклы.
Предыдущая статья
