如果您停留在旧版 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
我的基准测试表明,第二个变体在 Intel 处理器上速度更快,但在 AMD 上速度较慢(特别是 Athlon XP 和 Athlon 64)。我最终决定为我的库选择方法 #2,特别是因为我重新使用“调整”值来实现其他类型的快速舍入。
请注意,最后的 FISTP 指令同时支持 m32 和 m64 操作数,因此如果您想截断为 64 位整数以获得更高的精度,这是可能的。只要记住在堆栈上分配两倍的空间,然后使用fistp QWORD PTR, [esp] 而不是fistp DWORD PTR, [esp]。
我意识到这一切看起来都非常复杂,但是 这确实比调整舍入模式、进行舍入和设置舍入模式要快得多。我已经在各种处理器和各种代码路径中对它进行了广泛的基准测试,从未发现它变慢了。但我在 C 代码中使用它,标准要求编译器发出恢复舍入模式的代码。 如果您是手动编写汇编,并且需要截断,只需将 FPU 的舍入模式切换为“截断”一次并保留它。
这个位旋转代码也有一个双精度版本。关键是要意识到符号位位于 64 位双精度的高 32 位,因此您仍然只需要一个 32 位寄存器。
但是,双精度版本并非没有错误!非常接近整数的浮点值将向上舍入到最接近的整数,而不是被截断(eg,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