Здесь происходит немного истории.Попробуйте запустить
mov rax, -1
mov eax, 0
print rax
на вашем любимом рабочем столе x86 (print
в зависимости от вашей среды).Вы заметите, что, хотя rax
начинался со всех, и вы думаете, что уничтожили только 32 нижних бита, оператор print
выводит ноль!Пишет eax
полностью стереть rax
.Зачем?Потому что это намного быстрее.Попытка сохранить более высокие значения rax
является абсолютной болью, когда вы продолжаете писать в eax
.
Intel / AMD не помнили об этом, когда решили перейти на 32-разрядную версию, и сделалиошибка, из-за которой al
/ ah
не использовался за пределами загрузчиков и других битовых фиддлеров: когда вы пишете в al
или ah
, другой не забивается!Это было здорово в 16-битную эпоху, потому что теперь у вас в два раза больше регистров, и , у вас есть 32-битный регистр!Но с переходом на изобилие регистров мы больше не хотим больше регистров.Нам нужны быстрые регистры и больше ГГц.С этой точки зрения, каждый раз, когда вы пишете в al
или ah
, вам все равно придется читать из eax
.Это создает большую нагрузку на испорченный исполнитель.
Достаточно теории, давайте проведем несколько реальных тестов.Каждый был проверен три раза.Эти тесты выполнялись в Intel Core i5-4278U CPU @ 2.60GHz
Время: 1,067 с, 1,072 с, 1,097 с
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov rax, 5
mov rax, 5
mov rax, 6
mov rax, 6
mov rax, 7
mov rax, 7
mov rax, 8
mov rax, 8
dec ecx
jmp loop
exit:
ret
Время: 1,072 с, 1,062 с, 1,060 с
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov eax, 5
mov eax, 5
mov eax, 6
mov eax, 6
mov eax, 7
mov eax, 7
mov eax, 8
mov eax, 8
dec ecx
jmp loop
exit:
ret
Время: 2.702 с, 2.748 с, 2.704 с
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov ah, 5
mov ah, 6
mov ah, 6
mov ah, 7
mov ah, 7
mov ah, 8
mov ah, 8
dec ecx
jmp loop
exit:
ret
Время: 1.432 с, 1.457 с, 1.427 с
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
mov ah, 6
mov al, 6
mov ah, 7
mov al, 7
mov ah, 8
mov al, 8
dec ecx
jmp loop
exit:
ret
Время: 1.117 с, 1.084 с, 1.082s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
mov eax, 6
mov al, 6
mov ah, 7
mov eax, 7
mov ah, 8
mov al, 8
dec ecx
jmp loop
exit:
ret
Эти тесты не связаны с частичной остановкой регистра, так как я не читаю eax
после записи в ah
.Это просто стоимость работы с 8 или 16 битами, когда ваша шина 32 или 64 бит.В моем случае шина моего процессора 64-битная.32-битные записи не пострадали, так как это действительно 64-битная запись.Есть неявные 32 бита нулей, которые добавляются к каждой записи в eax.Это важно, так как большая часть кода скомпилирована в x86, и это должно быть быстродействующим в системах x86_64.
Кроме того, если вы захотите попробовать, вы заметите, что add eax, 5
и add ah, 5
оба принимаютстолько же времени (2,7 с на моем процессоре, столько же, сколько mov ah, 5
).В этом случае вам все равно придется читать с eax
, поэтому нет никакой разницы.Разница в том, что mov ah, 5
не должно требовать чтения, но это все же требуется.Это то, чем mov eax, 5
извлекает выгоду, но ah
не может.
В тесте подстановки ah / al мы видим, что переименование регистров, вероятно, помогает со всеми "mov ah, 5;Мов ал, 5 "пишет.Похоже, что «ах» и «ал» имеют свои собственные регистры для работы, и их можно затем сделать параллельными, что значительно экономит время.С тестом ah / al / eax он был почти таким же быстрым, как тест eax!В этом случае я предсказываю, что все три получили свои собственные регистры, и код был сильно распараллелен, даже когда отдельные записи в ах / ал были дорогими.Конечно, попытка прочитать eax
в любом месте этого цикла приведет к снижению производительности, когда необходимо объединить ах / ал:
Время: 3,412 с, 3,390 с, 3,515 с
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
xor eax, 5
mov al, 6
mov ah, 8
xor eax, 5
mov al, 8
dec ecx
jmp loop
exit:
ret
Вышеупомянутый тест не имеет контрольной группы, так как он использует xor вместо mov (что, если просто использовать «xor», является причиной, почему он медленный).Итак, вот тест для сравнения:
Время: 1,426 с, 1,424 с, 1,392 с
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
xor ah, 5
mov al, 6
mov ah, 8
xor ah, 5
mov al, 8
dec ecx
jmp loop
exit:
ret
Последние два теста показывают частичную остановку регистра, которую я даже не рассматривал вначале.Сначала я подумал, что переименование регистра поможет смягчить проблему, что они определенно делают в миксах ах / ал и мих ах / ал / эакс.Тем не менее, чтение по eax с грязными значениями ah / al является жестоким, поскольку процессор теперь должен объединять регистры ah / al.Похоже, что производители процессоров считали, что переименование регистров частичных регистров все же стоило того, что имеет смысл, так как большая часть работы с ah / al не включает чтение в eax.Таким образом, узкие циклы, которые немного ломаются от ах / ал, значительно выигрывают, и единственный вред - сбой при следующем использовании eax (в этот момент ах / ал, вероятно, больше не будет использоваться).
В целом, даже в случае отсутствия частичного останова регистра записи в ah
выполняются намного медленнее, чем записи в eax
, что я и пытался донести.
Конечно, результаты могутварьироваться.Другие процессоры (скорее всего, очень старые) могут иметь управляющие биты для отключения половины шины, что позволит шине работать как 8-битная шина, когда это необходимо.Эти управляющие биты должны были бы быть соединены через логические вентили с регистрами (то есть подключены к флагу сброса триггеров), что значительно замедлило бы их, поскольку теперь это еще один выход, через который можно обновить регистр.Поскольку такие контрольные биты будут подавляющим большинством времени, похоже, что Intel решила этого не делать (по уважительной причине).