答案 0 :(得分:6)
你真的有很多选择:
如果您还在使用 SSE2 指令,则可以使用SSE2指令将浮点值转换为带截断的整数值。 Peter Cordes's answer讨论了这种方法。 CVTTSD2SI
是标量版本, CVTTPD2DQ
是打包/矢量版本。
如果您的目标是x86-64,那么SSE2 总是可用,这就是应该用于所有浮点运算的内容。 x86 FPU在x86-64上完全过时了。
如果您在Pentium 4或Athlon 64之前定位x86-32处理器,则无法使用SSE2指令。在这种情况下, SSE 指令可能仍然可用(Pentium 3,Athlon XP及更高版本支持SSE)。 SSE仅支持单精度浮点运算,因此如果您不需要精度,可以使用 CVTTSS2SI
(标量)或 CVTTPS2DQ
(打包/矢量)。不幸的是,你经常需要精确度;请参阅下面的更好的解决方法。
如果 SSE3 指令可用(Pentium 4 Prescott,某些Athlon 64及更高版本),那么您可以使用 FISTTP
指令,这与FISTP
类似,只是它总是截断,不管当前的舍入模式如何。这是fuz's answer提出的解决方案。
如果您已经在使用x87 FPU,这是一个非常好的解决方案,但适用性有限,因为如果您的目标是支持SSE3的芯片,它们必然支持SSE2,因此您应该使用SSE指令来完成所有操作浮点操纵。唯一的例外是,如果确实需要x87 FPU提供的扩展80位精度用于中间计算(SSE2限制为64位双精度)。
如果你坚持使用传统的x86-32处理器并使用没有SSE的 x87 FPU ,你仍然没有选择。有一些快速比特方法。这些不是我最初的创新 - 代码散布在互联网的各个地方,我只是稍微整理和调整它们,所以我不能完全信任,也不能引用特定的来源。 Here is one such source
对于单精度浮点值,整个位表示适合32位寄存器,因此实现很简单(假设要截断的浮点值位于x87 FPU堆栈的顶部) ):
; Retrieve the bit representation of the original floating-point value.
push eax
fst DWORD PTR [esp]
mov eax, DWORD PTR [esp]
; Twiddle those raw bits.
and eax, 080000000H
xor eax, 0BEFFFFFFH
; Store those manipulated bits back in memory, since we can't load
; directly from a register to the x87 FPU stack.
mov DWORD PTR [esp], eax
; Add the modified value to the original value at the top of the stack.
fadd DWORD PTR [esp]
; Round the adjusted floating-point value to an integer.
; (Our bit manipulation ensures that this will always truncate,
; regardless of the current rounding mode.)
fistp DWORD PTR [esp]
; ... do something with the result in ESP
pop eax
另一种实现使用静态数组“调整”值,我们根据原始浮点值的“符号”将其编入索引。这基本上是用C编写的一个天真的“截断”函数,除了它无分支地执行:
const uint32_t kSingleAdjustments[2] = { 0xBEFFFFFF, /* -0.49999997f */
0x3EFFFFFF /* +0.49999997f */ };
; Retrieve the bit representation of the floating-point value.
push eax
fst DWORD PTR [esp]
mov eax, DWORD PTR [esp]
; Isolate the sign bit.
shr eax, 31
; Use the sign bit as an index into the array of values to add the appropriate
; adjustment value to the original floating-point value at the top of the stack.
; (NOTE: This syntax is for MSVC's inline asm; translate as necessary.)
fadd DWORD PTR [kSingleAdjustments + (eax * TYPE kSingleAdjustments)]
; Round the adjusted floating-point value to an integer.
; (Our adjustment ensures that it will be truncated, regardless of rounding mode.)
fistp DWORD PTR [esp]
; ... do something with the result in ESP
pop eax
我的基准测试表明,第二种变体在英特尔处理器上更快,但在AMD(特别是Athlon XP和Athlon 64)上更慢。我最终选择了我的库的方法#2,特别是因为我重新使用“调整”值来实现其他类型的快速舍入。
请注意,最终的FISTP
指令同时支持m32
和m64
个操作数,因此如果要截断为64位整数以获得更高的精度,那么这是可能的。请记住在堆栈上分配两倍的空间,然后使用fistp QWORD PTR, [esp]
代替fistp DWORD PTR, [esp]
。
我意识到这一切看起来都非常复杂,但这确实比调整舍入模式,进行舍入以及设置舍入模式要快得多。我已经在各种处理器和各种代码路径上对其进行了广泛的基准测试,从未发现它更慢。但我在C代码中使用它,标准需要编译器发出恢复舍入模式的代码。 如果您正在手动编写程序集,并且需要截断,只需将FPU的舍入模式切换为“截断”一次,然后将其保留。
这个bit-twiddling代码也有双精度版本。关键是要意识到符号位位于64位双精度的高32位,所以你仍然只需要一个32位寄存器。
但是,双精度版本不是无错误的!非常接近整数的浮点值将向上舍入到最接近的整数,而不是被截断(例如,4.99999977被错误地舍入为5,而不是被截断为4 )。比我更聪明,有更多时间玩这个可能会找到解决这个问题的方法,但在大多数情况下我对这种准确性感到满意,特别是考虑到速度的大幅提升。
const uint64_t kDoubleAdjustments[2] = { 0xBFDFFFFF00000000,
0x3FDFFFFF00000000 };
sub esp, 8
fst QWORD PTR [esp]
mov eax, DWORD PTR [esp+4] ; we only need the upper 32 bits
shr eax, 31
fadd QWORD PTR [kDoubleAdjustments + (eax * TYPE kDoubleAdjustments)]
fistp DWORD PTR [esp]
; ... do something with the result in ESP
add esp, 8
答案 1 :(得分:4)
SSE3 instruction set还引入了fisttp
指令。它的作用类似于fistp
指令,它可以将浮点数存储为32位整数(在进程中弹出堆栈),除了它总是截断值,不管它是什么当前的舍入模式。
以下是如何使用该示例的示例:
FLD QWORD PTR [esi] ; load 64 bit floating point number
FISTTP DWORD PTR [edi] ; truncate and store as 32 bit integer
或AT& T-syntax:
fldl (%esi)
fisttpl (%edi)
如果您没有支持SSE3的处理器,在确保将舍入模式设置为“truncate”后,您可以使用fistp
指令获得类似的结果。
sub esp,0x4 ; make space for the control word
fstcw WORD PTR [esp] ; store the FPU control word
fstcw WORD PTR [esp+0x2] ; store another copy
or WORD PTR [esp],0x0c00 ; set rounding mode to "truncate"
fldcw WORD PTR [esp] ; load updated control word
fld QWORD PTR [esi] ; load floating point number
fistp WORD PTR [edi] ; truncate to integer
fldcw WORD PTR [esp+0x2] ; restore control word
或AT& T-syntax:
sub $4,%esp
fstcw (%esp)
fstcw 2(%esp)
orw $0x0c00,(%esp)
fldcw (%esp)
fldl (%esi)
fistp (%edi)
fldcw 2(%esp)
如果您的代码不能在80286或更早的版本上运行,您可能希望使用fnstcw
而不是fstcw
来保存每条指令一个字节,代价是可能无法处理的代码一个真正的8087。
答案 2 :(得分:2)