如何在x86(32位)汇编中将无符号整数转换为浮点数?

6

我需要将32位和64位的无符号整数转换为xmm寄存器中的浮点值。x86指令可以将有符号整数转换为单精度和双精度浮点值,但没有用于无符号整数的指令。

奖励内容:如何将xmm寄存器中的浮点值转换为32位和64位无符号整数?


3
对于32位无符号整数而言,这很容易。但是对于64位有符号和无符号整数而言就较为困难。 - Mysticial
同样地,对于float->int转换,如果你愿意在NaNINF、溢出等方面做出一些妥协,那么有非常快速的方法。 - Mysticial
这是为编译器而设计的...所以不打算走捷径。 - tgiphil
2
我想唯一的方法就是将其分解为低32位和高32位。对于float->int转换,你需要分支来捕获所有边角情况。(或者使用条件移动进行黑客操作) - Mysticial
3个回答

4

不要介意我把Janus的答案当作模板(毕竟我很喜欢C++):

在 i7 上使用 gcc -march=native -O3 编译生成,因此包括 -mavx

uint2float 和相反转换都是预期的,长整型转换只有在数值大于263-1时才有特殊情况。

0000000000000000 <ulong2double>:
   0:   48 85 ff                test   %rdi,%rdi
   3:   78 0b                   js     10 <ulong2double+0x10>
   5:   c4 e1 fb 2a c7          vcvtsi2sd %rdi,%xmm0,%xmm0
   a:   c3                      retq   
   b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  10:   48 89 f8                mov    %rdi,%rax
  13:   83 e7 01                and    $0x1,%edi
  16:   48 d1 e8                shr    %rax
  19:   48 09 f8                or     %rdi,%rax
  1c:   c4 e1 fb 2a c0          vcvtsi2sd %rax,%xmm0,%xmm0
  21:   c5 fb 58 c0             vaddsd %xmm0,%xmm0,%xmm0
  25:   c3                      retq   

0000000000000030 <ulong2float>:
  30:   48 85 ff                test   %rdi,%rdi
  33:   78 0b                   js     40 <ulong2float+0x10>
  35:   c4 e1 fa 2a c7          vcvtsi2ss %rdi,%xmm0,%xmm0
  3a:   c3                      retq   
  3b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
  40:   48 89 f8                mov    %rdi,%rax
  43:   83 e7 01                and    $0x1,%edi
  46:   48 d1 e8                shr    %rax
  49:   48 09 f8                or     %rdi,%rax
  4c:   c4 e1 fa 2a c0          vcvtsi2ss %rax,%xmm0,%xmm0
  51:   c5 fa 58 c0             vaddss %xmm0,%xmm0,%xmm0
  55:   c3                      retq   

0000000000000060 <uint2double>:
  60:   89 ff                   mov    %edi,%edi
  62:   c4 e1 fb 2a c7          vcvtsi2sd %rdi,%xmm0,%xmm0
  67:   c3                      retq   

0000000000000070 <uint2float>:
  70:   89 ff                   mov    %edi,%edi
  72:   c4 e1 fa 2a c7          vcvtsi2ss %rdi,%xmm0,%xmm0
  77:   c3                      retq 

1
你只需要使用-march=core2-m64(也许是隐式的,就像你的情况一样)就可以得到这个结果。这里的所有AVX指令都有传统的SSE2变体。例如,最后一个vcvtsi2ss %rdi,%xmm0,%xmm0可以是cvtsi2ss %rdi,%xmm0。有趣的是,这在SSE1中也可以工作,但uint2double中的cvtsi2sd需要SSE2。 - Janus Troelsen
只使用32位指令? - tgiphil
@tgiphil:如果你的GCC默认为-m64,请将-m32添加到编译选项中以生成32位代码。 - Peter Cordes

3

这是GCC生成的代码。我将它们封装在函数中,但您可以轻松删除堆栈处理。并非所有代码都使用SSE来执行实际工作(ulonglong转换不使用),如果您找到相应的指令,请告诉我。Clang几乎相同。

% cat tofloats.c 
double ulonglong2double(unsigned long long a) {
    return a;
}
float ulonglong2float(unsigned long long a) {
    return a;
}
double uint2double(unsigned int a) {
    return a;
}
float uint2float(unsigned int a) {
    return a;
}
% gcc -msse4.2 -g -Os -c tofloats.c && objdump -d tofloats.o
00000000 <ulonglong2double>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   83 ec 10                sub    $0x10,%esp
   6:   8b 55 0c                mov    0xc(%ebp),%edx
   9:   8b 45 08                mov    0x8(%ebp),%eax
   c:   89 55 f4                mov    %edx,-0xc(%ebp)
   f:   85 d2                   test   %edx,%edx
  11:   89 45 f0                mov    %eax,-0x10(%ebp)
  14:   df 6d f0                fildll -0x10(%ebp)
  17:   79 06                   jns    1f <ulonglong2double+0x1f>
  19:   d8 05 00 00 00 00       fadds  0x0
  1f:   dd 5d f8                fstpl  -0x8(%ebp)
  22:   dd 45 f8                fldl   -0x8(%ebp)
  25:   c9                      leave  
  26:   c3                      ret    

00000027 <ulonglong2float>:
  27:   55                      push   %ebp
  28:   89 e5                   mov    %esp,%ebp
  2a:   83 ec 10                sub    $0x10,%esp
  2d:   8b 55 0c                mov    0xc(%ebp),%edx
  30:   8b 45 08                mov    0x8(%ebp),%eax
  33:   89 55 f4                mov    %edx,-0xc(%ebp)
  36:   85 d2                   test   %edx,%edx
  38:   89 45 f0                mov    %eax,-0x10(%ebp)
  3b:   df 6d f0                fildll -0x10(%ebp)
  3e:   79 06                   jns    46 <ulonglong2float+0x1f>
  40:   d8 05 00 00 00 00       fadds  0x0
  46:   d9 5d fc                fstps  -0x4(%ebp)
  49:   d9 45 fc                flds   -0x4(%ebp)
  4c:   c9                      leave  
  4d:   c3                      ret    

0000004e <uint2double>:
  4e:   55                      push   %ebp
  4f:   89 e5                   mov    %esp,%ebp
  51:   83 ec 08                sub    $0x8,%esp
  54:   66 0f 6e 45 08          movd   0x8(%ebp),%xmm0
  59:   66 0f d6 45 f8          movq   %xmm0,-0x8(%ebp)
  5e:   df 6d f8                fildll -0x8(%ebp)
  61:   c9                      leave  
  62:   c3                      ret    

00000063 <uint2float>:
  63:   55                      push   %ebp
  64:   89 e5                   mov    %esp,%ebp
  66:   83 ec 08                sub    $0x8,%esp
  69:   66 0f 6e 45 08          movd   0x8(%ebp),%xmm0
  6e:   66 0f d6 45 f8          movq   %xmm0,-0x8(%ebp)
  73:   df 6d f8                fildll -0x8(%ebp)
  76:   c9                      leave  
  77:   c3                      ret

以下是奖励积分(转换为整数):
% cat toints.c                                      
unsigned long long float2ulonglong(float a) {
    return a;
}
unsigned long long double2ulonglong(double a) {
    return a;
}
unsigned int float2uint(float a) {
    return a;
}
unsigned int double2uint(double a) {
    return a;
}
% gcc -msse4.2 -g -Os -c toints.c && objdump -d toints.o  
00000000 <float2ulonglong>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   53                      push   %ebx
   4:   83 ec 0c                sub    $0xc,%esp
   7:   d9 45 08                flds   0x8(%ebp)
   a:   d9 05 00 00 00 00       flds   0x0
  10:   d9 c9                   fxch   %st(1)
  12:   db e9                   fucomi %st(1),%st
  14:   73 0d                   jae    23 <float2ulonglong+0x23>
  16:   dd d9                   fstp   %st(1)
  18:   dd 4d f0                fisttpll -0x10(%ebp)
  1b:   8b 45 f0                mov    -0x10(%ebp),%eax
  1e:   8b 55 f4                mov    -0xc(%ebp),%edx
  21:   eb 13                   jmp    36 <float2ulonglong+0x36>
  23:   de e1                   fsubp  %st,%st(1)
  25:   dd 4d f0                fisttpll -0x10(%ebp)
  28:   8b 55 f4                mov    -0xc(%ebp),%edx
  2b:   8b 45 f0                mov    -0x10(%ebp),%eax
  2e:   8d 8a 00 00 00 80       lea    -0x80000000(%edx),%ecx
  34:   89 ca                   mov    %ecx,%edx
  36:   83 c4 0c                add    $0xc,%esp
  39:   5b                      pop    %ebx
  3a:   5d                      pop    %ebp
  3b:   c3                      ret    

0000003c <double2ulonglong>:
  3c:   55                      push   %ebp
  3d:   89 e5                   mov    %esp,%ebp
  3f:   53                      push   %ebx
  40:   83 ec 0c                sub    $0xc,%esp
  43:   dd 45 08                fldl   0x8(%ebp)
  46:   d9 05 00 00 00 00       flds   0x0
  4c:   d9 c9                   fxch   %st(1)
  4e:   db e9                   fucomi %st(1),%st
  50:   73 0d                   jae    5f <double2ulonglong+0x23>
  52:   dd d9                   fstp   %st(1)
  54:   dd 4d f0                fisttpll -0x10(%ebp)
  57:   8b 45 f0                mov    -0x10(%ebp),%eax
  5a:   8b 55 f4                mov    -0xc(%ebp),%edx
  5d:   eb 13                   jmp    72 <double2ulonglong+0x36>
  5f:   de e1                   fsubp  %st,%st(1)
  61:   dd 4d f0                fisttpll -0x10(%ebp)
  64:   8b 55 f4                mov    -0xc(%ebp),%edx
  67:   8b 45 f0                mov    -0x10(%ebp),%eax
  6a:   8d 8a 00 00 00 80       lea    -0x80000000(%edx),%ecx
  70:   89 ca                   mov    %ecx,%edx
  72:   83 c4 0c                add    $0xc,%esp
  75:   5b                      pop    %ebx
  76:   5d                      pop    %ebp
  77:   c3                      ret    

00000078 <float2uint>:
  78:   55                      push   %ebp
  79:   89 e5                   mov    %esp,%ebp
  7b:   83 ec 08                sub    $0x8,%esp
  7e:   d9 45 08                flds   0x8(%ebp)
  81:   dd 4d f8                fisttpll -0x8(%ebp)
  84:   8b 45 f8                mov    -0x8(%ebp),%eax
  87:   c9                      leave  
  88:   c3                      ret    

00000089 <double2uint>:
  89:   55                      push   %ebp
  8a:   89 e5                   mov    %esp,%ebp
  8c:   83 ec 08                sub    $0x8,%esp
  8f:   dd 45 08                fldl   0x8(%ebp)
  92:   dd 4d f8                fisttpll -0x8(%ebp)
  95:   8b 45 f8                mov    -0x8(%ebp),%eax
  98:   c9                      leave  
  99:   c3                      ret    

这些函数从堆栈中获取输入并将其返回到堆栈上。如果您需要在函数结束时将结果存储在XMM寄存器中,可以使用movd / movq将它们从堆栈中取出到XMM寄存器中。如果函数返回一个double,则结果位于-0x8(%ebp)。如果是float,则结果位于-0x4(%ebp)。Ulonglongs的长度与双精度浮点数相同,而ints的长度与单精度浮点数相同。

fisttpll:截断存储整数

FISTTP使用截断(chop)作为舍入模式将ST中的值转换为带符号整数,将结果传输到目标,并弹出ST。FISTTP接受字、短整型和长整型目标。

fucomi:比较浮点数值并设置EFLAGS

对寄存器ST(0)和ST(i)的内容执行无序比较,并根据结果在EFLAGS寄存器中设置状态标志ZF、PF和CF(请参见下表)。对于比较,零的符号被忽略,因此-0.0等于+0.0。


回答的方法很有趣;然而问题是将一个无符号整数加载到XMM寄存器中。 - tgiphil
我会接受那个答案;但是有没有一种方法可以在没有任何x87 FP寄存器的情况下完成这个操作? - tgiphil
1
请注意,fisttp 是在SSE3中引入的,尽管它操作的是x87寄存器。https://www.felixcloutier.com/x86/fisttp。如果没有SSE3,您可以使用当前的FP舍入模式(通常是最近)与`fistp`一起使用,而不是截断。或者像编译器在SSE1/2标量数学之前所做的那样更改FP舍入模式,然后再改回来,这包括引入了像`cvttsd2si`这样的截断转换。(转换为有限宽度的有符号整数,因此在32位模式下没有64位整数,不像x87) - Peter Cordes
@tgiphil:软件浮点运算是可能的,但速度会慢得多。也许还可以通过FP乘以1.0 / 2^32并在两个32位半中使用XMM FP->整数转换,但这也比将数据反弹到x87寄存器中要慢。除非你有AVX-512用于打包转换为uint64_t(https://www.felixcloutier.com/x86/vcvtpd2uqq),因此`VCVTPD2UQQ xmm1,xmm1/vmovq [esp],xmm1(或movd/pextrd`直接将两个32位半字移动到整数寄存器)。 - Peter Cordes

2
另请参见如何使用SSE/AVX高效执行double/int64转换?,以获取有限范围或全范围的打包转换技巧。还请参见奇怪的uint32_t到float数组转换,以分析编译器实现它的策略。该问题的其他答案只展示编译器生成的代码,没有讨论为什么它能工作。
AVX-512提供了将float/double转换为无符号64位整数的几乎等效的指令。本答案主要介绍double到uint64_t的转换,但是也存在类似的指令vcvtuqq2ps(打包的uint64_t转换为打包的单精度)以及vcvtusi2sd xmm1, xmm2, r/m64{er}等指令,只有在64位模式下才可用。与SSE1相同,这些指令具有不方便的合并到某个寄存器的语义,而不是零扩展到一个新的xmm寄存器中。

使用AVX-512F将floatdouble转换为uint64_t

AVX-512F增加了对浮点数与无符号整数的支持(标量vcvttsd2usi或打包)。还添加了有符号或无符号64位整数的打包转换(例如vcvttpd2uqq打包双精度或vcvttps2uqq将float32转换为uint64_t)。

在AVX-512之前,在64位模式下或使用x87时,使用零扩展到非负64位有符号整数可以轻松完成32位无符号整数的标量转换。但是64位无符号整数即使在64位模式下也是一个问题。

vcvttsd2usi eax, xmm0在AVX-512中可在32位或64位模式下工作。(或使用ss版本;float vs. double)

"

vcttvsd2usi rax, xmm0

当然只在64位模式下使用AVX-512才有效。因此,我们可以使用打包转换,因为处于32位模式并不会阻止64位SIMD整数元素的工作。

我不确定高半部分的垃圾是否会影响速度,表示子规格浮点/双精度。我猜可能不会,因为将其舍入为整数与微小的标准化值没有区别。

"
;;; 32-bit mode can use packed 64-bit conversion then get the 2 halves
 vcvttpd2uqq  xmm1, xmm0    ; 2x truncating uint64_t from double conversions

 vmovd       eax, xmm1       ; extract the halves to integer registers
 vpextrd     edx, xmm1, 1    ; edx:eax = (uint64_t)xmm0[0]

直接将其存储到内存中,使用vmovq [esp],xmm1
如果要使用当前的舍入模式而不是截断,则在助记符中省略额外的t。(如果您没有更改MXCSR,则默认舍入模式为最接近的偶数作为平局。)
vcvtpd2uqq  xmm1, xmm0       ; (uint64_t)nearbyint(xmm0)

所有新的AVX-512 FP转换指令都提供了截断和当前舍入模式版本。这对于打包转换来说是有意义的;EVEX舍入模式覆盖仅适用于ZMMM向量,而不适用于XMM/YMM。

我有点惊讶他们费心制作vcvttsd2usi的操作码,而不是只是将其作为VCVTSD2USI r64,xmm1 / m64 {rz-sae}的别名来覆盖舍入模式以朝向零的截断。 (您还可以覆盖到最近,向上或向下)。这会抑制该指令的FP异常,因此也许他们想支持检测通过检查MXCSR标志不精确或溢出的转换的代码。


不幸的是,“趋势排序”将此答案置于使用SSE或AVX的旧答案之前,而没有AVX-512,这仍然很少见,因为英特尔没有费心定义一种CPU提供有用新操作的方式,而不必同时实现512位向量宽度。无论如何,请参阅其他答案,了解如何让编译器为这些情况生成可工作的代码。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接