接受+0.0或-0.0时最快比较双精度到0的方法

时间:2017-10-16 02:24:25

标签: c++ optimization floating-point comparison zero

到目前为止,我有以下内容:

bool IsZero(const double x) {
  return fabs(x) == +0.0;
}

这是最快的正确比较精确0的方式,而+0.0-0.0都被接受了吗?

如果是CPU特定的,我们考虑x86-64。如果编译器特定,请考虑MSVC ++ 2017工具集v141。

3 个答案:

答案 0 :(得分:2)

简单来说,如果你想要接受+0.0和-0.0,你只需要使用:

x == 0.0

OR

您可以使用cmath库:

int fpclassify(double arg)将返回"零"对于-0.0或+0.0

答案 1 :(得分:2)

既然你说你想要最快的代码,我将在整个答案中做出一些重要的简化假设。根据这个问题,这些都是合法的。特别是,我假设x86和IEEE-754表示浮点值。在适用的情况下,我还会提到特定于MSVC的怪癖,尽管一般性讨论适用于任何针对此架构的编译器。

测试浮点值是否等于零的方法是测试其所有位。如果所有位都为0,则该值为零。实际上,该值为+0.0。符号位可以是0或1,因为表示允许正数和负数0.0,正如您在问题中提到的那样。但是这种差异并不存在实际上存在(实际上并没有+0.0和-0.0这样的东西),所以你真正需要的是测试除之外的所有位 符号位。

这可以快速有效地完成,但有点麻烦。在像x86这样的小端架构上,符号位是前导位,因此您只需将其移出然后测试剩余的位。

这个技巧由Agner Fog在他的Optimizing Subroutines in Assembly Language中描述。具体来说,示例17.4b(当前版本的第156页)。

对于32位宽的单精度浮点值(float):

mov   eax, DWORD PTR [floatingPointValue]
add   eax, eax        ; shift out the sign bit to ignore -0.0
sete  al              ; set AL if the remaining bits were 0

将其翻译成C代码,您可以执行以下操作:

const uint32_t bits = *(reinterpret_cast<uint32_t*>(&value));
return ((bits + bits) == 0);

当然,由于类型惩罚,这在形式上是不安全的。 MSVC让你逃脱它,没问题。事实上,如果你试图真正符合标准并保证安全,那么MSVC将倾向于生成 less 高效代码,从而降低了这一技巧的有效性。如果您想安全地执行此操作,则需要验证编译器的输出并确保它正在执行您想要的操作。还建议使用一些断言。

如果你对这种方法的不安全特性感到满意,你会发现它 比一个预测不佳的条件分支更快,所以当你处理随机的时候输入值,可能是表现胜利。为了进行比较,如果您只是对0.0的平等进行天真测试,那么您将从MSVC中看到以下内容:

  ;; assuming /arch:IA32, which is *not* the default in modern versions of MSVC
  ;; but necessary if you cannot assume SSE2 support
  fld      DWORD PTR [floatingPointValue]
  fldz
  fucompp
  fnstsw   ax
  test     ah, 44h
  jp       IsNonZero
  mov      al, 1
  ret
IsNonZero:
  xor      al, al
  ret
  ;; assuming /arch:SSE2, which *is* the default in modern versions of MSVC
  movss    xmm0, DWORD PTR [floatingPointValue]
  ucomiss  xmm0, DWORD PTR [constantZero]
  lahf
  test     ah, 44h
  jp       IsNonZero
  mov      al, 1
  ret
IsNonZero:
  xor      al, al
  ret

丑陋,可能很慢。有无分支的方法,但MSVC不会使用它们。

&#34;优化&#34;明显的缺点上面描述的实现是它需要从存储器加载浮点值以便访问它的位。没有x87指令可以直接访问这些位,并且没有经过内存就无法从x87寄存器直接进入GP寄存器。由于内存访问速度很慢,这会导致性能下降,但在我的测试中,它仍然比错误预测的分支更快。

如果你在32位x86(__cdecl__stdcall等)上使用任何标准调用约定,那么所有浮点值都会传递并返回x87寄存器,因此从x87寄存器移到GP寄存器与从x87寄存器移到SSE寄存器没有区别。

如果你的目标是x86-64,或者你在x86-32上使用__vectorcall,那么故事会有所不同。然后,您实际上在SSE寄存器中存储并传递了浮点值,因此您可以利用无分支SSE指令。至少在理论上。除非你握住它,否则MSVC不会赢。它通常会执行上面显示的相同分支比较,只是没有额外的内存负载:

  ;; MSVC output for a __vectorcall function, targeting x86-32 with /arch:SSE2
  ;; and/or for x86-64 (which always uses a vector calling convention and SSE2)
  ;; The floating point value being compared is passed directly in XMM0
  ucomiss   xmm0, DWORD PTR [constantZero]
  lahf
  test      ah, 44h
  jp       IsNonZero
  mov      al, 1
  ret
IsNonZero:
  xor      al, al
  ret

我已经演示了一个非常简单的bool IsZero(float val)函数的编译器输出,但在我的观察中,MSVC总是为这种类型的比较发出UCOMISS + JP序列,无论比较如何结合到输入代码中。再次,如果输入的零点是可预测的,那么很好,但如果分支预测失败则相对糟糕。

如果你想确保你获得无分支代码,避免分支错误预测失速的可能性,那么你需要使用内在函数进行比较。这些内在函数将迫使MSVC发出更接近您期望的代码:

return (_mm_ucomieq_ss(_mm_set_ss(floatingPointValue), _mm_setzero_ps()) != 0);

不幸的是,输出仍然不完美。你遇到了关于内在函数使用的一般优化缺陷 - 即各种SSE寄存器之间的输入值的一些冗余混洗 - 但这是(A)不可避免的,(B)不是可测量的性能问题。

我在这里注意到其他编译器,如Clang和GCC,不需要他们的手。你可以value == 0.0。它们发出的确切代码序列会有所不同,具体取决于您的优化设置,但您会看到COMISS + SETEUCOMISS + SETNP + {{ 1}}或CMOVNE + CMPEQSS + MOVD(后者仅由ICC使用)。你试图用内在函数来控制几乎肯定会导致输出效率降低,所以这可能需要NEG来限制它到MSVC。

这个单精度值,宽度为32位。那么两倍长度的双精度值怎么样?您认为这些将有63位进行测试(因为符号位仍然被忽略),但是有一个扭曲。如果你可以排除非正规数字的可能性,那么你可以只测试高位(再次假设是小端)。

Agner Fog也讨论了这个问题(例17.4d)。如果排除非正规数的可能性,则值0对应于指数位全为0的情况。高位是符号位和指数位,因此您可以像对单个位一样测试这些 - 精度值:

#ifdef

在不安全的C:

mov    eax, DWORD PTR [floatingPointValue+4]  ; load upper bits only
add    eax, eax        ; shift out sign bit to ignore -0.0
sete   al              ; set AL if the remaining bits were 0

如果你需要考虑非正常值,那么你就不能保存自己的任何东西。我还没有对此进行测试,但是你可能不会让编译器生成代码以进行天真的比较。至少,不适用于x86-32。您可能仍然可以在x86-64上获得,其中有64位宽的GP寄存器。

如果您可以假设SSE2支持(这将是所有x86-64系统,以及所有现代x86-32版本),那么您只需使用内在函数,并获得免费的非正常支持(嗯,不是真正免费;我相信CPU中存在内部惩罚,但我们会忽略这些惩罚:

const uint64_t bits      = *(reinterpret_cast<uint64_t*>(&value);
const uint32_t upperBits = (bits & 0xFFFFFFFF00000000) >> 32;
return ((upperBits + upperBits) == 0);

同样,与单精度值一样,在MSVC以外的编译器上不需要使用内在函数来获得最佳代码,实际上可能会导致代码次优,因此应该避免使用。

答案 2 :(得分:0)

如果打开代码的汇编程序,您可以找到用于不同版本代码的汇编程序指令。拥有汇编程序,您可以估计哪个更好。

在GCC编译器中,您可以通过以下方式保留中间文件(包括汇编程序版本):

  

gcc -save-temps main.cpp