【问题标题】:Are there unsigned equivalents of the x87 FILD and SSE CVTSI2SD instructions?是否有 x87 FILD 和 SSE CVTSI2SD 指令的无符号等效项?
【发布时间】:2012-11-23 22:13:02
【问题描述】:

我想在 GHC Haskell 编译器中实现相当于 C 的 uint-to-double 转换。我们已经使用FILDCVTSI2SD 实现了int-to-double。这些操作是否有无符号版本,或者我应该在转换之前将uint 的最高位归零(从而丢失范围)?

【问题讨论】:

标签: assembly floating-point sse floating-point-conversion x87


【解决方案1】:

正如有人所说,“好艺术家抄袭;伟大艺术家偷窃”。所以我们可以看看其他编译器作者是如何解决这个问题的。我用了一个简单的sn-p:

volatile unsigned int x;
int main()
{
  volatile double  y = x;
  return y;
}

(添加挥发物以确保编译器不会优化转换)

结果(跳过无关指令):

Visual C++ 2010 cl /Ox (x86)

  __real@41f0000000000000 DQ 041f0000000000000r ; 4.29497e+009

  mov   eax, DWORD PTR ?x@@3IC          ; x
  fild  DWORD PTR ?x@@3IC           ; x
  test  eax, eax
  jns   SHORT $LN4@main
  fadd  QWORD PTR __real@41f0000000000000
$LN4@main:
  fstp  QWORD PTR _y$[esp+8]

所以基本上编译器会添加一个调整值,以防符号位被设置。

Visual C++ 2010 cl /Ox (x64)

  mov   eax, DWORD PTR ?x@@3IC          ; x
  pxor  xmm0, xmm0
  cvtsi2sd xmm0, rax
  movsdx    QWORD PTR y$[rsp], xmm0

这里不需要调整,因为编译器知道rax会清除符号位。

Visual C++ 2012 cl /Ox

  __xmm@41f00000000000000000000000000000 DB 00H, 00H, 00H, 00H, 00H, 00H, 00H
  DB 00H, 00H, 00H, 00H, 00H, 00H, 00H, 0f0H, 'A'

  mov   eax, DWORD PTR ?x@@3IC          ; x
  movd  xmm0, eax
  cvtdq2pd xmm0, xmm0
  shr   eax, 31                 ; 0000001fH
  addsd xmm0, QWORD PTR __xmm@41f00000000000000000000000000000[eax*8]
  movsd QWORD PTR _y$[esp+8], xmm0

这使用无分支代码添加 0 或根据符号位是否被清除或设置进行魔术调整。

【讨论】:

  • 不同的编译器使用不同的策略(除了 x86-64 上的 u32 -> double,所有编译器都知道零扩展为 i64)。 godbolt.org/z/MPe9hsP99 显示 32 位模式 GCC 和 SSE2 的叮当声。
【解决方案2】:

您可以利用 IEEE 双精度格式的某些属性并将无符号值解释为尾数的一部分,同时添加一些精心设计的指数。

Bits 63 62-52     51-0
     S  Exp       Mantissa
     0  1075      20 bits 0, followed by your unsigned int

1075 来自双精度数的 IEEE 指数偏差 (1023) 和尾数的 52 位“移位”量。注意尾数前面有一个隐含的“1”,后面需要减去。

所以:

double uint32_to_double(uint32_t x) {
    uint64_t xx = x;
    xx += 1075ULL << 52;         // add the exponent
    double d = *(double*)&xx;    // or use a union to convert
    return d - (1ULL << 52);     // 2 ^^ 52
}

如果您的平台上没有原生 64 位,则使用 SSE 进行整数步长的版本可能会有所帮助,但这当然取决于。

在我的平台上编译为

0000000000000000 <uint32_to_double>:
   0:   48 b8 00 00 00 00 00    movabs $0x4330000000000000,%rax
   7:   00 30 43 
   a:   89 ff                   mov    %edi,%edi
   c:   48 01 f8                add    %rdi,%rax
   f:   c4 e1 f9 6e c0          vmovq  %rax,%xmm0
  14:   c5 fb 5c 05 00 00 00    vsubsd 0x0(%rip),%xmm0,%xmm0 
  1b:   00 
  1c:   c3                      retq

看起来不错。 0x0(%rip) 是神奇的双精度常量,如果内联一些指令,如高位 32 位归零和常量重载,就会消失。

【讨论】:

【解决方案3】:

有更好的办法

__m128d _mm_cvtsu32_sd(__m128i n) {
    const __m128i magic_mask = _mm_set_epi32(0, 0, 0x43300000, 0);
    const __m128d magic_bias = _mm_set_sd(4503599627370496.0);
    return _mm_sub_sd(_mm_castsi128_pd(_mm_or_si128(n, magic_mask)), magic_bias);
}

【讨论】:

  • 我认为如果你能阐明聪明的代码的作用会很棒。
  • @NorbertP。它与我给出的算法基本相同,只是作为纯 SSE 代码。
【解决方案4】:

我们已经使用 FILD 实现了 int-to-double ...
这些操作是否有无符号版本

如果您想使用 x87 FILD 操作码,只需将 uint64 转换为 uint63(div 2),然后将其乘以 2,但已经是 double,因此 x87 uint64 到 double 的转换需要在开销中执行一次 FMUL .

例子: 0xFFFFFFFFFFFFFFFFU -> +1.8446744073709551e+0019

无法以严格的表单规则发布代码示例。我稍后再试。

    //inline
    double    u64_to_d(unsigned _int64 v){

    //volatile double   res;
    volatile unsigned int tmp=2;
    _asm{
    fild  dword ptr tmp
    //v>>=1;
    shr   dword ptr v+4, 1
    rcr   dword ptr v, 1
    fild  qword ptr v

    //save lsb
    //mov   byte ptr tmp, 0  
    //rcl   byte ptr tmp, 1

    //res=tmp+res*2;
    fmulp st(1),st
    //fild  dword ptr tmp
    //faddp st(1),st 

    //fstp  qword ptr res
    }

    //return res;
    //fld  qword ptr res
}

VC 产生 x86 输出

        //inline
        double    u64_to_d(unsigned _int64 v){
    55                   push        ebp  
    8B EC                mov         ebp,esp  
    81 EC 04 00 00 00    sub         esp,04h  

        //volatile double   res;
        volatile unsigned int tmp=2;
    C7 45 FC 02 00 00 00 mov         dword ptr [tmp], 2  
        _asm{
        fild  dword ptr tmp
    DB 45 FC             fild        dword ptr [tmp]  
        //v>>=1;
        shr   dword ptr v+4, 1
    D1 6D 0C             shr         dword ptr [ebp+0Ch],1  
        rcr   dword ptr v, 1
    D1 5D 08             rcr         dword ptr [v],1  
        fild  qword ptr v
    DF 6D 08             fild        qword ptr [v]  

        //save lsb
    //    mov   byte ptr [tmp], 0  
    //C6 45 FC 00        mov         byte ptr [tmp], 0
    //    rcl   byte ptr tmp, 1
    //D0 55 FC           rcl         byte ptr [tmp],1  

        //res=tmp+res*2;
        fmulp st(1),st
    DE C9                fmulp       st(1),st  
    //    fild  dword ptr tmp
    //DB 45 FC           fild        dword ptr [tmp]  
    //    faddp st(1),st 
    //DE C1              faddp       st(1),st  


        //fstp  qword ptr res
        //fstp        qword ptr [res]  
    }

        //return res;
        //fld         qword ptr [res]  

    8B E5                mov         esp,ebp  
    5D                   pop         ebp  
    C3                   ret  
}

我发布了(可能是我手动删除了文本文件中所有不正确的 ascii 字符)。

【讨论】:

    【解决方案5】:

    如果我对您的理解正确,您应该能够将您的 32 位 uint 移动到堆栈上的临时区域,将下一个 dword 清零,然后使用 fild qword ptr 将现在的 64 位无符号整数加载为双倍。

    【讨论】:

    • 这会导致存储转发停顿(两个狭窄存储的广泛重新加载),但是可以,这可以作为 x87 方式来执行正常的 x86-64 技术,将零扩展到 64 -cvtsi2sd xmm, rax 的位寄存器。
    【解决方案6】:

    在 AVX-512 之前,x86 没有无符号 FP 指令。
    (对于 AVX-512F,请参阅 vcvtusi2sdvcvtsd2usi,以及它们各自的 ss 版本。还打包了涉及 64 位整数的 SIMD 转换,这也是新的;在 AVX-512F 之前,打包转换可以去往/来自int32_t.)


    在 64 位代码中,无符号 32 位 -> FP 很简单:只需将 u32 零扩展为 i64 并使用有符号 64 位转换。 每个 uint32_t 值都可以表示为非负 int64_t。

    对于反方向,如果您对超出范围的 FP 输入的情况感到满意,请转换 FP -> i64 并截断为 u32。 (包括 i64 超出范围时的 0,否则取 2 的补码 i64 位模式的 low32。)


    u32 -> FP:请参阅@Igor Skochinsky 对编译器输出的回答。 x86-64 GCC 和 Clang 使用与 x64 MSVC 相同的技巧。关键部分是将其零扩展为 64 位并进行转换。请注意writing a 32-bit register implicitly zero-extends to 64-bit,因此如果您知道该值是使用 32 位操作写入的,则可能不需要mov r32, r32。 (或者如果您必须自己从内存中加载它)。

    ; assuming your input starts in EDI, and that RDI might have garbage in the high half
    ; like a 32-bit function arg.
    
        mov     eax, edi              ; mov-elimination wouldn't work with  edi,edi
        vcvtsi2sd xmm0, xmm7, rax     ; where XMM7 is some cold register to avoid a false dep
    

    选择 mov edi,edi 以外的任何内容(如果您需要单独的零扩展指令)的动机是 mov-elimination 不能在相同的寄存器情况下工作:请参阅Can x86's MOV really be "free"? Why can't I reproduce this at all?

    如果您没有 AVX,或者不知道要使用的最近未编写的寄存器,您可能希望在设计不佳的 cvtsi2sd 合并到它之前使用 pxor xmm0, xmm0。 GCC 虔诚地打破了错误的 dep,clang 非常随意,除非循环携带的 dep 链将存在于单个函数中。因此,它可能会因单独的非内联函数之间的交互而减慢,这些函数可能碰巧在循环中被调用。请参阅 Why does adding an xorps instruction make this function using cvtsi2ss and addss ~5x faster? 以了解这会叮叮当当的示例(但 GCC 很好。)

    该答案还链接了一些 GCC 错过优化错误报告,我在其中写了更多关于重用“冷”寄存器以避免转换中的错误依赖关系以及类似 [v]sqrtsd 的内容的详细信息,这也是一个 1 输入操作。


    32位模式:

    不同的编译器有不同的策略。 gcc -O3 -m32 -mfpmath=sse -msseregparm 是查看 GCC 功能的好方法,使其返回 XMM0 而不是 ST0,因此它仅在实际上更方便时才使用 x87。 (例如,对于 64 位 -> FP 使用 fild)。

    我用 gcc 和 clang 放了一些 u32 和 u64 -> 浮点或双重测试函数 on Godbolt,但这个答案主要是为了回答问题的 x86-64 部分,其他答案没有很好地涵盖,不是过时的 32 位代码生成器。所以这里就不复制代码和asm来剖析了。

    我会提到double 可以精确地表示每个u32,这允许一个简单的(double)(int)(u32 - 2^31) + double(2^31) 技巧来进行范围移位以进行有符号转换。但是u32->float可没那么容易。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-12-08
    • 1970-01-01
    • 2017-03-20
    • 2011-09-19
    • 2011-04-14
    • 2016-06-29
    • 1970-01-01
    • 2016-04-15
    相关资源
    最近更新 更多