Полностью зная, что эти полностью искусственные тесты не имеют большого значения, я, тем не менее, немного удивлен несколькими способами, которые компиляторы "большой четверки" выбрали для создания тривиального фрагмента.
struct In {
bool in1;
bool in2;
};
void foo(In &in) {
extern bool out1;
extern bool out2;
out1 = (in.in1 == true);
out2 = in.in2;
}
Примечание: все компиляторы установлены в режиме x64 с наивысшим "общим назначением" (= не указана конкретная архитектура процессора) "оптимизация по скорости"; Вы можете увидеть результаты самостоятельно/поиграть с ними на https://gcc.godbolt.org/z/K_i8h9)
Clang 6 с -O3, по-видимому, дает наиболее простой результат:
foo(In&): # @foo(In&)
mov al, byte ptr [rdi]
mov byte ptr [rip + out1], al
mov al, byte ptr [rdi + 1]
mov byte ptr [rip + out2], al
ret
В стандартной программе-совместимый C++ на == true
сравнение является излишним, так что оба задания становятся прямыми копьями из одного места памяти в другую, не проходя через al
, как там нет памяти для памяти mov
.
Однако, поскольку здесь нет никакого давления в регистре, я ожидал, что он будет использовать два разных регистра (чтобы полностью избежать ложных цепочек зависимостей между двумя присваиваниями), возможно, сначала начав все чтения, а затем выполнив все записи после, чтобы помочь инструкции -уровневый параллелизм; этот вид оптимизации полностью устарел с недавними процессорами из-за переименования регистров и агрессивно вышедших из строя процессоров? (подробнее об этом позже)
GCC 8.2 с -O3 делает почти то же самое, но с изюминкой:
foo(In&):
movzx eax, BYTE PTR [rdi]
mov BYTE PTR out1[rip], al
movzx eax, BYTE PTR [rdi+1]
mov BYTE PTR out2[rip], al
ret
Вместо простого mov
к "маленькому" регистру, он делает movzx
для полного eax
. Зачем? Является ли это полным сбросом состояния eax
и подрегистров в переименователе регистров, чтобы избежать частичных остановок регистров?
MSVC 19 с /O2 добавляет еще одну причуду:
in$ = 8
void foo(In & __ptr64) PROC ; foo, COMDAT
cmp BYTE PTR [rcx], 1
sete BYTE PTR bool out1 ; out1
movzx eax, BYTE PTR [rcx+1]
mov BYTE PTR bool out2, al ; out2
ret 0
void foo(In & __ptr64) ENDP ; foo
Помимо другого соглашения о вызовах, здесь второе назначение почти одинаково.
Однако сравнение в первом присваивании фактически выполняется (что интересно, используя как cmp
и sete
с операндами памяти, так что вы можете сказать, что промежуточный регистр - это FLAGS).
- Является ли этот V C++ явным образом безопасным (программист попросил об этом, может, он знает что-то, чего я не знаю об этом
bool
) или это связано с некоторыми известными внутренними ограничениями - например,bool
рассматривается как обычный байт без особых свойств сразу после интерфейса? - Поскольку это не "настоящая" ветвь (путь кода не изменяется в результате выполнения
cmp
), я ожидаю, что это не будет стоить так дорого, особенно по сравнению с доступом к памяти. Насколько затратна эта пропущенная оптимизация?
Наконец, ICC 18 с -O3 является самым странным из всех:
foo(In&):
xor eax, eax #9.5
cmp BYTE PTR [rdi], 1 #9.5
mov dl, BYTE PTR [1+rdi] #10.12
sete al #9.5
mov BYTE PTR out1[rip], al #9.5
mov BYTE PTR out2[rip], dl #10.5
ret #11.1
- Первое назначение делает сравнение, точно так, как в V C++ коде, но
sete
проходит черезal
вместо того, чтобы прямо в память; есть ли причина предпочитать это? - Все чтения "запускаются", прежде чем что-либо делать с результатами - так что этот вид чередования все еще имеет значение?
- Почему
eax
обнуляется в начале функции? Частичный регистр снова глохнет? Но тогдаdl
не получает это лечение...
Ради интереса я попытался удалить == true
, а теперь ICC
foo(In&):
mov al, BYTE PTR [rdi] #9.13
mov dl, BYTE PTR [1+rdi] #10.12
mov BYTE PTR out1[rip], al #9.5
mov BYTE PTR out2[rip], dl #10.5
ret #11.1
Таким образом, нет нуля из eax
, но по-прежнему с использованием двух регистров и "сначала начать чтение параллельно, потом использовать все результаты".
- Что такого особенного в
sete
которая заставляет ICC думать, что стоит обнулитьeax
раньше? - Правильно ли, в конце концов, ICC переупорядочивать операции чтения/записи подобным образом, или очевидно более небрежный подход других компиляторов в настоящее время выполняет то же самое?