GCC 在将整数转换为浮点数期间生成的 FPU 操作
FPU operations generated by GCC during casting integer to float
我想在 C 中对 FPU 执行除法(使用整数值):
float foo;
uint32_t *ptr1, *ptr2;
foo = (float)*(ptr1) / (float)*(ptr2);
并且在 NASM 中(来自通过 GCC 编译的对象)它具有以下表示:
mov rax, QWORD [ptr1]
mov eax, DWORD [rax]
mov eax, eax
test rax, rax
js ?_001
pxor xmm0, xmm0
cvtsi2ss xmm0, rax
jmp ?_002
?_001:
mov rdx, rax
shr rdx, 1
and eax, 01H
or rdx, rax
pxor xmm0, xmm0
cvtsi2ss xmm0, rdx
addss xmm0, xmm0
?_002:
mov rax, QWORD [ptr2]
; ... for ptr2 pattern repeats
下面的"black magic"是什么意思?_001是什么意思?仅 cvtsi2ss 是否足以从整数转换为浮点数?
一般来说,cvtsi2ss 可以解决问题 - 将标量整数(其他来源将其命名为双字整数为单个标量,但我的命名与其他向量 ins 一致)转换为单个标量(浮点数)。但它需要有符号整数。
所以这个代码
mov rdx, rax
shr rdx, 1
and eax, 01H
or rdx, rax
pxor xmm0, xmm0
cvtsi2ss xmm0, rdx
addss xmm0, xmm0
帮助将无符号转换为有符号(请注意 js 跳转 - 如果设置了符号位,则执行此代码 - 否则跳过)。当 uint32_t.
的值大于 0x7FFFFFFF 时设置符号
因此 "magic" 代码执行:
mov rdx, rax ; move value from ptr1 to edx
shr rdx, 1 ; div by 2 - logic shift not arithmetic because ptr1 is unsigned
and eax, 01H ; save least significant bit
or rdx, rax ; move this bit to divided value to someway fix rounding errors
pxor xmm0, xmm0
cvtsi2ss xmm0, rdx
addss xmm0, xmm0 ; add to itself = multiply by 2
我不确定你使用的是什么编译器和编译选项 - GCC 只是
cvtsi2ssq xmm0, rbx
cvtsi2ssq xmm1, rax
divss xmm0, xmm1
希望对您有所帮助。
您一定是在查看未优化的 代码。那是浪费时间。当优化器被禁用时,编译器会出于各种原因生成一堆无意义的代码——为了获得更快的编译速度,为了更容易在源代码行上设置断点,为了更容易捕获错误等等。
当您在针对 x86-64 的编译器上生成 优化 代码时,所有这些噪音都会消失,代码变得更加高效,因此更容易 interpret/understand.
这是一个执行您想要的操作的函数。我将它写成一个函数,这样我就可以将输入作为不透明参数传递,编译器无法优化它。
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
这几乎正是您所期望看到的。这里唯一的 "black magic" 是 PXOR
指令。为什么编译器在执行 CVTSI2SS
指令之前费心将 XMM 寄存器预置零,而该指令无论如何都会破坏它们?好吧,因为 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 中丢失)。
然而,这并不是说 (and ) is entirely incorrect. CVTSI2SS
确实设计用于将 有符号 整数转换为标量单精度浮点数,而不是 unsigned integer 就像你在这里。那么给出了什么?好吧,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
由于优化器重新排列和交错指令的方式,这有点难以理解。在这里,我 "unoptimized" 它重新排序指令并将它们分解为更多逻辑组,希望更容易遵循执行流程。 (我删除的唯一指令是破坏依赖性的指令 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 位输入值一次(请参阅指令的两个 "groups")。然后,最后,将得到的两个浮点表示相除,结果为returned.
逻辑类似于未优化的代码所做的,但是……好吧,更优化。特别是,冗余指令被删除,算法被推广,因此不需要对符号性进行分支。这加快了速度,因为错误预测的分支很慢。
请注意,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
将这些双精度浮点值中的每一个转换为单精度浮点值。最后,它将两个单精度浮点数相除,并排列成return值。
因此,当以 32 位为目标时,编译器确实必须处理有符号整数和无符号整数之间的差异,但它们以不同的方式处理,使用不同的策略——有些可能比其他的更优化。这就是为什么查看优化后的代码会更有启发性,除了它实际上是在您的客户端机器上执行的。
我想在 C 中对 FPU 执行除法(使用整数值):
float foo;
uint32_t *ptr1, *ptr2;
foo = (float)*(ptr1) / (float)*(ptr2);
并且在 NASM 中(来自通过 GCC 编译的对象)它具有以下表示:
mov rax, QWORD [ptr1]
mov eax, DWORD [rax]
mov eax, eax
test rax, rax
js ?_001
pxor xmm0, xmm0
cvtsi2ss xmm0, rax
jmp ?_002
?_001:
mov rdx, rax
shr rdx, 1
and eax, 01H
or rdx, rax
pxor xmm0, xmm0
cvtsi2ss xmm0, rdx
addss xmm0, xmm0
?_002:
mov rax, QWORD [ptr2]
; ... for ptr2 pattern repeats
下面的"black magic"是什么意思?_001是什么意思?仅 cvtsi2ss 是否足以从整数转换为浮点数?
一般来说,cvtsi2ss 可以解决问题 - 将标量整数(其他来源将其命名为双字整数为单个标量,但我的命名与其他向量 ins 一致)转换为单个标量(浮点数)。但它需要有符号整数。
所以这个代码
mov rdx, rax
shr rdx, 1
and eax, 01H
or rdx, rax
pxor xmm0, xmm0
cvtsi2ss xmm0, rdx
addss xmm0, xmm0
帮助将无符号转换为有符号(请注意 js 跳转 - 如果设置了符号位,则执行此代码 - 否则跳过)。当 uint32_t.
的值大于 0x7FFFFFFF 时设置符号因此 "magic" 代码执行:
mov rdx, rax ; move value from ptr1 to edx
shr rdx, 1 ; div by 2 - logic shift not arithmetic because ptr1 is unsigned
and eax, 01H ; save least significant bit
or rdx, rax ; move this bit to divided value to someway fix rounding errors
pxor xmm0, xmm0
cvtsi2ss xmm0, rdx
addss xmm0, xmm0 ; add to itself = multiply by 2
我不确定你使用的是什么编译器和编译选项 - GCC 只是
cvtsi2ssq xmm0, rbx
cvtsi2ssq xmm1, rax
divss xmm0, xmm1
希望对您有所帮助。
您一定是在查看未优化的 代码。那是浪费时间。当优化器被禁用时,编译器会出于各种原因生成一堆无意义的代码——为了获得更快的编译速度,为了更容易在源代码行上设置断点,为了更容易捕获错误等等。
当您在针对 x86-64 的编译器上生成 优化 代码时,所有这些噪音都会消失,代码变得更加高效,因此更容易 interpret/understand.
这是一个执行您想要的操作的函数。我将它写成一个函数,这样我就可以将输入作为不透明参数传递,编译器无法优化它。
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
这几乎正是您所期望看到的。这里唯一的 "black magic" 是 PXOR
指令。为什么编译器在执行 CVTSI2SS
指令之前费心将 XMM 寄存器预置零,而该指令无论如何都会破坏它们?好吧,因为 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 中丢失)。
然而,这并不是说 CVTSI2SS
确实设计用于将 有符号 整数转换为标量单精度浮点数,而不是 unsigned integer 就像你在这里。那么给出了什么?好吧,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
由于优化器重新排列和交错指令的方式,这有点难以理解。在这里,我 "unoptimized" 它重新排序指令并将它们分解为更多逻辑组,希望更容易遵循执行流程。 (我删除的唯一指令是破坏依赖性的指令 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 位输入值一次(请参阅指令的两个 "groups")。然后,最后,将得到的两个浮点表示相除,结果为returned.
逻辑类似于未优化的代码所做的,但是……好吧,更优化。特别是,冗余指令被删除,算法被推广,因此不需要对符号性进行分支。这加快了速度,因为错误预测的分支很慢。
请注意,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
将这些双精度浮点值中的每一个转换为单精度浮点值。最后,它将两个单精度浮点数相除,并排列成return值。
因此,当以 32 位为目标时,编译器确实必须处理有符号整数和无符号整数之间的差异,但它们以不同的方式处理,使用不同的策略——有些可能比其他的更优化。这就是为什么查看优化后的代码会更有启发性,除了它实际上是在您的客户端机器上执行的。