奇怪的uint32_t浮点数组转换

时间:2016-10-19 17:37:12

标签: c++ visual-studio vectorization sse

我有以下代码段:

#include <cstdio>
#include <cstdint>

static const size_t ARR_SIZE = 129;

int main()
{
  uint32_t value = 2570980487;

  uint32_t arr[ARR_SIZE];
  for (int x = 0; x < ARR_SIZE; ++x)
    arr[x] = value;

  float arr_dst[ARR_SIZE];
  for (int x = 0; x < ARR_SIZE; ++x)
  {
    arr_dst[x] = static_cast<float>(arr[x]);
  }

  printf("%s\n", arr_dst[ARR_SIZE - 1] == arr_dst[ARR_SIZE - 2] ? "OK" : "WTF??!!");

  printf("magic = %0.10f\n", arr_dst[ARR_SIZE - 2]);
  printf("magic = %0.10f\n", arr_dst[ARR_SIZE - 1]);
  return 0;
}

如果我在MS Visual Studio 2015下编译它,我可以看到输出是:

WTF??!!
magic = 2570980352.0000000000
magic = 2570980608.0000000000

所以最后一个arr_dst元素与前一个元素不同,但这两个值是通过转换相同的值来获得的,这个值填充了arr数组! 这是一个错误吗?

我注意到如果我按以下方式修改转换循环,我会得到“OK”结果:

for (int x = 0; x < ARR_SIZE; ++x)
{
  if (x == 0)
    x = 0;
  arr_dst[x] = static_cast<float>(arr[x]);
}

所以这可能是矢量化优化的一些问题。

此行为无法在gcc 4.8上重现。有什么想法吗?

3 个答案:

答案 0 :(得分:5)

32位IEEE-754二进制浮点数(例如MSVC ++使用)仅提供6-7个十进制数字的精度。您的起始值完全在该类型的范围内,但似乎不能完全由该类型表示,因为大多数类型为uint32_t的值都是如此。

与此同时,x86或x86_64处理器的浮点单元使用比MSVC ++的64位double更宽的表示。似乎在循环退出后,最后计算的数组元素以其扩展精度形式保留在FPU寄存器中。然后,程序可以直接从寄存器中使用该值,而不是从存储器中读取它,这对于以前的元素是有义务的。

如果程序通过将较窄的表示提升到更广泛而不是相反的方式来执行==比较,则这两个值可能确实比较不等,如从扩展精度到{{1的往返并且后退失去精确度。无论如何,当传递给float时,这两个值都会转换为double类型;如果他们确实比较不平等,那么这些转换的结果也可能不同。

我没有使用MSVC ++编译选项,但很可能有一个可以解决这种行为。这些选项有时会使用诸如“严格数学”或“严格fp”之类的名称。但请注意,在FP重型程序中打开这样的选项(或关闭其相反的选项)可能会非常昂贵。

答案 1 :(得分:4)

在x86上,unsignedfloat之间的转换并不简单;它没有单一指令(直到AVX512)。一种常见的技术是转换为签名然后修复结果。有多种方法可以做到这一点。 (参见this Q&A for some manually-vectorized methods with C intrinsics,并非所有结果都有完美的结果。)

MSVC使用一个策略对前128个进行矢量化,然后对最后一个标量元素使用不同的策略(不会向量化),这涉及转换为double然后从double转换为{ {1}}。

gcc和clang从它们的矢量化和标量方法中产生float结果。 2570980608.02570980608 - 2570980487 = 121(没有输入/输出的舍入),因此gcc和clang在这种情况下产生正确的舍入结果(小于0.5ulp的错误)。 IDK如果对于每个可能的uint32_t都是如此(但它们只有2 ^ 32,we could exhaustively check)。 MSVC对向量化循环的最终结果略微超过0.5ulp的误差,但标量方法对于此输入正确舍入。

IEEE数学要求2570980487 - 2570980352 = 135 + - */产生正确的舍入结果(小于0.5ulp的误差),但其他功能(像sqrt)没有这么严格的要求。 IDK对于转换int-&gt;浮点转换的要求是什么,所以IDK如果MSVC做什么是严格合法的(如果你没有使用log或任何东西)。

另见Bruce Dawson的Floating-Point Determinism blog post(他关于FP数学的优秀系列的一部分),尽管他没有提到整数&lt; - &gt; FP转换。

我们可以在OP链接的asm中看到MSVC做了什么(只删除了有趣的指令并手工评论)

/fp:fast

......一些更为荒谬的已知编译时间后来跳......

; Function compile flags: /Ogtp
# assembler macro constants
_arr_dst$ = -1040                   ; size = 516
_arr$ = -520                        ; size = 516
_main   PROC                        ; COMDAT

  00013      mov     edx, 129
  00018      mov     eax, -1723986809   ; this is your unsigned 2570980487
  0001d      mov     ecx, edx
  00023      lea     edi, DWORD PTR _arr$[esp+1088]  ; edi=arr
  0002a      rep stosd             ; memset in chunks of 4B
  # arr[0..128] = 2570980487 at this point

  0002c      xor     ecx, ecx      ; i = 0
  # xmm2 = 0.0 in each element (i.e. all-zero)
  # xmm3 = __xmm@4f8000004f8000004f8000004f800000  (a constant repeated in each of 4 float elements)


  ####### The vectorized unsigned->float conversion strategy:
  $LL7@main:                                       ; do{
  00030      movups  xmm0, XMMWORD PTR _arr$[esp+ecx*4+1088]  ; load 4 uint32_t
  00038      cvtdq2ps xmm1, xmm0                 ; SIGNED int to Single-precision float
  0003b      movaps  xmm0, xmm1
  0003e      cmpltps xmm0, xmm2                  ; xmm0 = (xmm0 < 0.0)
  00042      andps   xmm0, xmm3                  ; mask the magic constant
  00045      addps   xmm0, xmm1                  ; x += (x<0.0) ? magic_constant : 0.0f;
   # There's no instruction for converting from unsigned to float, so compilers use inconvenient techniques like this to correct the result of converting as signed.
  00048      movups  XMMWORD PTR _arr_dst$[esp+ecx*4+1088], xmm0 ; store 4 floats to arr_dst
  ; and repeat the same thing again, with addresses that are 16B higher (+1104)
  ; i.e. this loop is unrolled by two

  0006a      add     ecx, 8         ;  i+=8 (two vectors of 4 elements)
  0006d      cmp     ecx, 128
  00073      jb  SHORT $LL7@main    ; }while(i<128)

 #### End of vectorized loop
 # and then IDK what MSVC smoking; both these values are known at compile time.  Is /Ogtp not full optimization?
 # I don't see a branch target that would let execution reach this code
 #  other than by falling out of the loop that ends with ecx=128
  00075      cmp     ecx, edx
  00077      jae     $LN21@main     ; if(i>=129): always false

  0007d      sub     edx, ecx       ; edx = 129-128 = 1

作为比较,clang和gcc也没有在编译时优化整个事情,但他们确实意识到他们不需要清理循环,只做一个标量存储或转换后的相应循环。 (clang实际上完全展开了所有内容,除非你告诉它不要。)

请参阅Godbolt compiler explorer上的代码。

gcc只是将上半部分和下半部分分别转换为浮动,并将它们与乘以65536相加并添加。

Clang的 ######## The scalar unsigned->float conversion strategy for the last element $LC15@main: 00140 mov eax, DWORD PTR _arr$[esp+ecx*4+1088] 00147 movd xmm0, eax # eax = xmm0[0] = arr[128] 0014b cvtdq2pd xmm0, xmm0 ; convert the last element TO DOUBLE 0014f shr eax, 31 ; shift the sign bit to bit 1, so eax = 0 or 1 ; then eax indexes a 16B constant, selecting either 0 or 0x41f0... (as whatever double that represents) 00152 addsd xmm0, QWORD PTR __xmm@41f00000000000000000000000000000[eax*8] 0015b cvtpd2ps xmm0, xmm0 ; double -> float 0015f movss DWORD PTR _arr_dst$[esp+ecx*4+1088], xmm0 ; and store it 00165 inc ecx ; ++i; 00166 cmp ecx, 129 ; } while(i<129) 0016c jb SHORT $LC15@main # Yes, this is a loop, which always runs exactly once for the last element - &gt; unsigned转换策略很有意思:它根本不使用float指令。我认为它将无符号整数的两个16位半部分直接填充到两个浮点数的尾数中(使用一些技巧来设置指数(按位布尔值和ADDPS),然后像gcc那样将低和高一半加在一起。

当然,如果编译为64位代码,标量转换只能将cvt零扩展到64位,并将其转换为带符号的int64_t为float。有符号的int64_t可以表示uint32_t的每个值,x86可以将64位有符号的int转换为有效的float。但这并没有矢量化。

答案 2 :(得分:2)

我对PowerPC imeplementation(飞思卡尔MCP7450)进行了调查,因为他们的抄本远比英特尔推出的任何伏都教都要好。

事实证明浮点单元,FPU和向量单元可能具有不同的浮点运算舍入。 FPU可以配置为使用四种舍入模式之一;舍入到最接近(默认),截断,朝向正无穷大并朝向负无穷大。然而,矢量单元仅能够舍入到最近,具有一些具有特定舍入规则的选择指令。 FPU的内部精度为106位。矢量单元符合IEEE-754标准,但文档说明不多。

查看结果,转换2570980608更接近原始整数,表明FPU具有比矢量单位或不同舍入模式更好的内部精度。