Вы, должно быть, видите неоптимизированный код. Это пустая трата времени. Когда оптимизатор отключен, компиляторы генерируют кучу бессмысленного кода по разным причинам — для достижения более высокой скорости компиляции, для упрощения установки точек останова на строках исходного кода, для облегчения отлова ошибок и т. д.
Когда вы создаете оптимизированный код на компиляторе, ориентированном на x86-64, весь этот шум исчезает, код становится намного эффективнее и, как следствие, его намного легче интерпретировать/понимать.
Вот функция, которая выполняет нужную вам операцию. Я написал это как функцию, поэтому я могу передавать входные данные как непрозрачные параметры, и компилятор не может их оптимизировать.
float DivideAsFloat(uint32_t *ptr1, uint32_t *ptr2)
{
return (float)(*ptr1) / (float)(*ptr2);
}
Вот объектный код, который все версии GCC (начиная с 4.9.0) генерируют для этой функции:
DivideAsFloat(unsigned int*, unsigned int*):
mov eax, DWORD PTR [rdi] ; retrieve value of 'ptr1' parameter
pxor xmm0, xmm0 ; zero-out xmm0 register
pxor xmm1, xmm1 ; zero-out xmm1 register
cvtsi2ssq xmm0, rax ; convert *ptr1 into a floating-point value in XMM0
mov eax, DWORD PTR [rsi] ; retrieve value of 'ptr2' parameter
cvtsi2ssq xmm1, rax ; convert *ptr2 into a floating-point value in XMM1
divss xmm0, xmm1 ; divide the two floating-point values
ret
Это почти то, что вы ожидаете увидеть. Единственная «черная магия» здесь — это PXOR
инструкции. Почему компилятор утруждает себя предварительным обнулением регистров XMM перед выполнением инструкции CVTSI2SS
, которая все равно их уничтожит? Ну, потому что CVTSI2SS
только частично затирает регистр назначения. В частности, он затирает только младшие биты, оставляя верхние биты нетронутыми. Это приводит к ложной зависимости от старших битов, что приводит к остановке выполнения. Эту зависимость можно разорвать, предварительно обнулив регистр, что предотвратит возможность зависаний и ускорит выполнение. Инструкция PXOR
— это быстрый и эффективный способ очистки регистра. (Я недавно говорил об этом точно таком же явлении здесь—см. последний абзац.)
Фактически, более старые версии GCC (до 4.9.0) не выполняли эту оптимизацию и, таким образом, генерировали код, который не включал инструкции PXOR
. Он выглядит более эффективным, но на самом деле работает медленнее.
DivideAsFloat(unsigned int*, unsigned int*):
mov eax, DWORD PTR [rdi] ; retrieve value of 'ptr1' parameter
cvtsi2ssq xmm0, rax ; convert *ptr1 into a floating-point value in XMM0
mov eax, DWORD PTR [rsi] ; retrieve value of 'ptr2' parameter
cvtsi2ssq xmm1, rax ; convert *ptr2 into a floating-point value in XMM1
divss xmm0, xmm1 ; divide the two floating-point values
ret
Clang 3.9 выдает тот же код, что и эти старые версии GCC. Он также не знает об оптимизации. MSVC знает об этом (начиная с VS 2010), как и современные версии ICC (проверено на ICC 16 и более поздних версиях; отсутствует в ICC 13).
Однако это не означает, что ответ Энти (и комментарий Mystical) совершенно неверен. CVTSI2SS
действительно предназначен для преобразования целого числа со знаком в скалярное число с плавающей запятой одинарной точности, а не целое число unsigned, как здесь. Так что дает? Ну, 64-битный процессор имеет доступные 64-битные регистры, поэтому 32-битные входные значения без знака могут храниться как 64-битные промежуточные значения со знаком, что, в конце концов, позволяет использовать CVTSI2SS
.
Компиляторы делают это, когда оптимизация включена, потому что это приводит к более эффективному коду. Если, с другой стороны, вы ориентируетесь на 32-разрядную архитектуру x86 и не имеете доступных 64-разрядных регистров, компилятору придется решать проблему со знаком и без знака. Вот как с этим справляется GCC 6.3:
DivideAsFloat(unsigned int*, unsigned int*):
sub esp, 4
pxor xmm0, xmm0
mov eax, DWORD PTR [esp+8]
pxor xmm1, xmm1
movss xmm3, 1199570944
pxor xmm2, xmm2
mov eax, DWORD PTR [eax]
movzx edx, ax
shr eax, 16
cvtsi2ss xmm0, eax
mov eax, DWORD PTR [esp+12]
cvtsi2ss xmm1, edx
mov eax, DWORD PTR [eax]
movzx edx, ax
shr eax, 16
cvtsi2ss xmm2, edx
mulss xmm0, xmm3
addss xmm0, xmm1
pxor xmm1, xmm1
cvtsi2ss xmm1, eax
mulss xmm1, xmm3
addss xmm1, xmm2
divss xmm0, xmm1
movss DWORD PTR [esp], xmm0
fld DWORD PTR [esp]
add esp, 4
ret
Это немного сложно понять из-за того, как оптимизатор переупорядочивает и чередует инструкции. Здесь я «неоптимизировал» его, переупорядочив инструкции и разбив их на более логические группы в надежде упростить отслеживание потока выполнения. (Единственной инструкцией, которую я удалил, была PXOR
, разрушающая зависимости — остальная часть кода осталась прежней, просто перегруппирована.)
DivideAsFloat(unsigned int*, unsigned int*):
;;; Initialization ;;;
sub esp, 4 ; reserve 4 bytes on the stack
pxor xmm0, xmm0 ; zero-out XMM0
pxor xmm1, xmm1 ; zero-out XMM1
pxor xmm2, xmm2 ; zero-out XMM2
movss xmm3, 1199570944 ; load a constant into XMM3
;;; Deal with the first value ('ptr1') ;;;
mov eax, DWORD PTR [esp+8] ; get the pointer specified in 'ptr1'
mov eax, DWORD PTR [eax] ; dereference the pointer specified by 'ptr1'
movzx edx, ax ; put the lower 16 bits of *ptr1 in EDX
shr eax, 16 ; move the upper 16 bits of *ptr1 down to the lower 16 bits in EAX
cvtsi2ss xmm0, eax ; convert the upper 16 bits of *ptr1 to a float
cvtsi2ss xmm1, edx ; convert the lower 16 bits of *ptr1 (now in EDX) to a float
mulss xmm0, xmm3 ; multiply FP-representation of upper 16 bits of *ptr1 by magic number
addss xmm0, xmm1 ; add the result to the FP-representation of *ptr1's lower 16 bits
;;; Deal with the second value ('ptr2') ;;;
mov eax, DWORD PTR [esp+12] ; get the pointer specified in 'ptr2'
mov eax, DWORD PTR [eax] ; dereference the pointer specified by 'ptr2'
movzx edx, ax ; put the lower 16 bits of *ptr2 in EDX
shr eax, 16 ; move the upper 16 bits of *ptr2 down to the lower 16 bits in EAX
cvtsi2ss xmm2, edx ; convert the lower 16 bits of *ptr2 (now in EDX) to a float
cvtsi2ss xmm1, eax ; convert the upper 16 bits of *ptr2 to a float
mulss xmm1, xmm3 ; multiply FP-representation of upper 16 bits of *ptr2 by magic number
addss xmm1, xmm2 ; add the result to the FP-representation of *ptr2's lower 16 bits
;;; Do the division, and return the result ;;;
divss xmm0, xmm1 ; FINALLY, divide the FP-representation of *ptr1 by *ptr2
movss DWORD PTR [esp], xmm0 ; store this result onto the stack, in the memory we reserved
fld DWORD PTR [esp] ; load this result onto the top of the x87 FPU
; (the 32-bit calling convention requires floating-point values be returned this way)
add esp, 4 ; clean up the space we allocated on the stack
ret
Обратите внимание, что стратегия здесь состоит в том, чтобы разбить каждое из 32-битных целых чисел без знака на две его 16-битные половины. Верхняя половина преобразуется в представление с плавающей запятой и умножается на магическое число (для компенсации знаковости). Затем нижняя половина преобразуется в представление с плавающей запятой, и эти два представления с плавающей запятой (каждая 16-битная половина исходного 32-битного значения) складываются вместе. Это делается дважды — по одному разу для каждого 32-битного входного значения (см. две «группы» инструкций). Затем, наконец, два результирующих представления с плавающей запятой делятся и возвращается результат.
Логика аналогична тому, что делает неоптимизированный код, но более оптимальна. В частности, удаляются избыточные инструкции, а алгоритм обобщается таким образом, что отпадает необходимость в ветвлении по подписке. Это ускоряет работу, потому что неправильно предсказанные ветки выполняются медленно.
Обратите внимание, что Clang использует немного другую стратегию и может генерировать здесь даже более оптимальный код, чем это делает GCC:
DivideAsFloat(unsigned int*, unsigned int*):
push eax ; reserve 4 bytes on the stack
mov eax, DWORD PTR [esp+12] ; get the pointer specified in 'ptr2'
mov ecx, DWORD PTR [esp+8] ; get the pointer specified in 'ptr1'
movsd xmm1, QWORD PTR 4841369599423283200 ; load a constant into XMM1
movd xmm0, DWORD PTR [ecx] ; dereference the pointer specified by 'ptr1',
; and load the bits directly into XMM0
movd xmm2, DWORD PTR [eax] ; dereference the pointer specified by 'ptr2'
; and load the bits directly into XMM2
orpd xmm0, xmm1 ; bitwise-OR *ptr1's raw bits with the magic number
orpd xmm2, xmm1 ; bitwise-OR *ptr2's raw bits with the magic number
subsd xmm0, xmm1 ; subtract the magic number from the result of the OR
subsd xmm2, xmm1 ; subtract the magic number from the result of the OR
cvtsd2ss xmm0, xmm0 ; convert *ptr1 from single-precision to double-precision in place
xorps xmm1, xmm1 ; zero register to break dependencies
cvtsd2ss xmm1, xmm2 ; convert *ptr2 from single-precision to double-precision, putting result in XMM1
divss xmm0, xmm1 ; FINALLY, do the division on the single-precision FP values
movss DWORD PTR [esp], xmm0 ; store this result onto the stack, in the memory we reserved
fld DWORD PTR [esp] ; load this result onto the top of the x87 FPU
; (the 32-bit calling convention requires floating-point values be returned this way)
pop eax ; clean up the space we allocated on the stack
ret
Он даже не использует инструкцию CVTSI2SS
! Вместо этого он загружает целые биты и манипулирует ими с помощью некоторого волшебного преобразования битов, поэтому он может рассматривать его как значение с плавающей запятой двойной точности. Немного поработав немного позже, он использует CVTSD2SS
для преобразования каждого из этих значений с плавающей запятой двойной точности в значения с плавающей запятой одинарной точности. Наконец, он делит два значения с плавающей запятой одинарной точности и организует возврат значения.
Так что, ориентируясь на 32-разрядные системы, компиляторам приходится иметь дело с разницей между целыми числами со знаком и без знака, но они делают это по-разному, используя разные стратегии — некоторые из них, вероятно, более оптимальны, чем другие. И именно поэтому просмотр оптимизированного кода гораздо поучительнее, если не считать того факта, что именно он будет фактически выполняться на машине вашего клиента.
person
Cody Gray
schedule
06.01.2017
cvtsi2ss
— это целочисленное преобразование со знаком. Нет беззнакового эквивалента. Так что компилятору нужно обойти это. - person Mysticial   schedule 06.01.2017