使用x87 FPU将浮点转换为带截断的整数,而不是舍入

时间:2016-12-13 00:54:53

标签: assembly nasm x87

FISTP instruction将0.75更改为1(因为四舍五入)

我希望0.75变成0而不是1。

FIST / FISTP 是否有替代圆形的替代方法?

3 个答案:

答案 0 :(得分:6)

你真的有很多选择:

  1. 如果您还在使用 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 (打包/矢量)。不幸的是,你经常需要精确度;请参阅下面的更好的解决方法。

  2. 如果 SSE3 指令可用(Pentium 4 Prescott,某些Athlon 64及更高版本),那么您可以使用 FISTTP 指令,这与FISTP类似,只是它总是截断,不管当前的舍入模式如何。这是fuz's answer提出的解决方案。

    如果您已经在使用x87 FPU,这是一个非常好的解决方案,但适用性有限,因为如果您的目标是支持SSE3的芯片,它们必然支持SSE2,因此您应该使用SSE指令来完成所有操作浮点操纵。唯一的例外是,如果确实需要x87 FPU提供的扩展80位精度用于中间计算(SSE2限制为64位双精度)。

  3. 如果你坚持使用传统的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指令同时支持m32m64个操作数,因此如果要截断为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)

如果您不必首先使用大部分过时的x87,那么所有SSE / SSE2转换指令(标量和打包)都有一个截断版本,可让您(和C编译器)有效地实现C语义而不改变舍入模式。

e.g。 CVTSD2SICVTTSD2SI
CVTPS2DQCVTTPS2DQ,所有都只需要SSE2。

CVTTSS2SI只需要SSE。

(SSE2在XMM寄存器中添加了对双精度浮点数和整数向量的支持,因此SSE1只有标量单浮点数到整数寄存器,而不是打包浮点到压缩DWORD向量。)

是的我知道OP说他们不能使用SSE,但这是这个问题标题的正确答案,并且希望对未来的搜索者有用。