什么是在x86上提供无分支FP min和max的指令?

时间:2016-10-22 20:38:31

标签: c assembly optimization floating-point x86

引用(感谢作者开发和共享算法!):

https://tavianator.com/fast-branchless-raybounding-box-intersections/

  

因为现代浮点指令集可以在没有分支的情况下计算最小值和最大值

作者的相应代码只是

dmnsn_min(double a, double b)
{
  return a < b ? a : b;
}

我熟悉例如_mm_max_ps,但这是一个矢量指令。上面的代码显然是用于标量形式。

问题:

  • x86上的标量无分支minmax指令是什么?这是一系列指令吗?
  • 假设它将被应用,或者如何调用它是否安全?
  • 关于min / max的无分支问题是否有意义?根据我的理解,对于光线跟踪器和/或其他视觉软件,给定光线盒交叉例程,分支预测器没有可靠的模式来拾取,因此消除分支确实有意义。我是对的吗?
  • 最重要的是,所讨论的算法是围绕(+/-)INFINITY进行比较而建立的。这是可靠的w.r.t我们正在讨论的(未知)指令和浮点标准吗?

以防万一:我熟悉Use of min and max functions in C++,相信它有关但不完全是我的问题。

2 个答案:

答案 0 :(得分:19)

大多数向量FP指令都有标量等价物。 MINSS / MAXSS / MINSD / MAXSD是您想要的。它们按照您期望的方式处理+/-无限。

MINSS a,b 完全根据IEEE规则实现(a<b) ? a : b,其中包含有关signed-zero,NaN和Infinities的所有内容。 (即它保持源操作数b无序。)这意味着编译器可以将它们用于std::min(b,a)std::max(b,a),因为这些函数基于相同的表达式。

MAXSS a,b 完全实现(b<a) ? a : b,再次保持源操作数无序。如果数组包含任何NaN,则使用maxss xmm0, [rsi]循环数组将导致NaN,通过计算传播NaN,这与其他FP操作一样正常。这也意味着您可以使用NaN(使用xmm0)而不是-Inf或第一个数组元素初始化pcmpeqd xmm0,xmm0;这可能会简化处理可能空的列表。

不要尝试在标量浮点数上使用_mm_min_ss;内在函数仅适用于__m128操作数Intel's intrinsics don't provide any way to get a scalar float into the low element of a __m128 without zeroing the high elements or somehow doing extra work.大多数编译器实际上会发出无用的指令来执行此操作,即使最终结果不依赖于上层中的任何内容元素。没有什么比__m256 _mm256_castps128_ps256 (__m128 a)更像是在上层元素中将一个浮点数转换为带有垃圾的__m128。我认为这是一个设计缺陷。 :/

但幸运的是,您不需要手动执行此操作,编译器知道如何为您使用SSE / SSE2 min / max。只需编写C即可。您的问题中的功能是理想的:如下所示(Godbolt链接):

// can and does inline to a single MINSD instruction, and can auto-vectorize easily
static inline double
dmnsn_min(double a, double b) {
  return a < b ? a : b;
}

注意它们使用NaN的非对称行为:如果操作数是无序的,则dest = src(即,如果任一操作数是NaN,则它采用第二个操作数)。这对SIMD条件更新很有用,见下文。

(如果其中任何一个是NaN,则ab都是无序的。这意味着a<ba==ba>b都是假的。请参阅{ {3}}。)

相应的_mm_min_ss / _mm_min_ps内在函数可能会也可能不会出现此行为,具体取决于编译器。

我认为内在函数应该具有与asm指令相同的操作数顺序语义,但是即使没有_mm_min_ps很长一段时间,gcc也将操作数视为可换向-ffast-math ,gcc4.4或者更早。 GCC 7最终将其更改为与ICC和clang匹配。

英特尔的在线内在搜索器并没有记录该功能的行为,但它可能不应该是详尽无遗的。 asm insn ref手册并没有说明内在的没有具有该属性;它只列出_mm_min_ss作为MINSS的内在函数。

当我在"_mm_min_ps" NaN上搜索时,我发现了Bruce Dawson's series of articles on floating point for lots of FP gotchas以及使用内在函数来处理NaN的其他一些讨论,所以很明显很多人都希望内在函数像asm指令一样。 (这是我昨天写的一些代码,我已经考虑过把它写成一个自我回答的Q&amp; A。)

鉴于存在这种长期存在的gcc错误,想要利用MINPS的NaN处理能力的便携式代码需要采取预防措施。许多现有Linux发行版上的标准gcc版本将错误编译您的代码,如果它取决于_mm_min_ps的操作数顺序。所以你可能需要一个#ifdef来检测实际的gcc(不是clang等),还有另一种选择。或者首先采用不同的方式:/或者使用_mm_cmplt_ps和布尔AND / ANDNOT / OR。

启用-ffast-math也会在所有编译器上进行_mm_min_ps交换。

像往常一样,编译器知道如何使用指令集正确实现C语义。 MINSS和MAXSS是this real code,因此只需编写可以编译为其中一个的代码。

可交换 - _mm_min_ps问题适用于内在:gcc确切地知道MINSS / MINPS如何工作,并使用它们来正确实现严格的FP语义(当你没有&#39;使用-ffast-math)。

您通常不需要做任何特殊的事情来从编译器中获取正确的标量代码。如果您打算花时间关注编译器使用的指令,那么如果编译器没有这样做,您应该首先手动对代码进行矢量化。

(在极少数情况下,分支最好,如果条件几乎总是单向,延迟比吞吐量更重要.MINPS延迟约为3个周期,但完美预测的分支会为依赖链增加0个周期关键路径。)

在C ++中,使用std::minstd::max,它们是根据><定义的,并且对NaN行为没有相同的要求fminfmax这样做。 除非您需要他们的NaN行为,否则请避免使用faster than anything you could do with a branch anyway

在C中,我认为只需编写自己的minmax函数(如果你安全地执行它们,就可以编写宏)。

fmin and fmax

float minfloat(float a, float b) {
  return (a<b) ? a : b;
}
# any decent compiler (gcc, clang, icc), without any -ffast-math or anything:
    minss   xmm0, xmm1
    ret

// C++
float minfloat_std(float a, float b) { return std::min(a,b); }
  # This implementation of std::min uses (b<a) : b : a;
  # So it can only produce the result in the register that b was in
  # This isn't worse (when inlined), just opposite
    minss   xmm1, xmm0
    movaps  xmm0, xmm1
    ret


float minfloat_fmin(float a, float b) { return fminf(a, b); }

# clang inlines fmin; other compilers just tailcall it.
minfloat_fmin(float, float):
    movaps  xmm2, xmm0
    cmpunordss      xmm2, xmm2
    movaps  xmm3, xmm2
    andps   xmm3, xmm1
    minss   xmm1, xmm0
    andnps  xmm2, xmm1
    orps    xmm2, xmm3
    movaps  xmm0, xmm2
    ret
   # Obviously you don't want this if you don't need it.

如果你想自己使用_mm_min_ss / _mm_min_ps,编写一些代码,让编译器在没有-ffast-math的情况下做得很好。

如果您不想要NaN,或者想要特别处理它们,请写下像

这样的内容
lowest = _mm_min_ps(lowest, some_loop_variable);

所以持有lowest的寄存器可以就地更新(即使没有AVX)。

利用MINPS的NaN行为:

说你的标量代码就像是

if(some condition)
    lowest = min(lowest, x);

假设条件可以使用CMPPS进行矢量化,因此您有一个元素向量,其中所有位都设置或全部清除。 (或者也许你可以直接使用ANDPS / ORPS / XORPS浮点数,如果你只关心他们的标志并且不关心负零。这会在符号位中创建一个真值,其他地方都有垃圾.BLENDVPS只看符号位,所以这可能非常有用。或者你可以用PSRAD xmm, 31广播符号位。)

实现此目标的直接方法是根据条件掩码将x+Inf混合。或者newval = min(lowest, x);并将newval混合到lowest。 (BLENDVPS或AND / ANDNOT / OR)。

但诀窍是全一位是NaN,而按位OR会传播它。所以:

__m128 inverse_condition = _mm_cmplt_ps(foo, bar);
__m128 x = whatever;


x = _mm_or_ps(x, condition);   // turn elements into NaN where the mask is all-ones
lowest = _mm_min_ps(x, lowest);  // NaN elements in x mean no change in lowest
//  REQUIRES NON-COMMUTATIVE _mm_min_ps: no -ffast-math
//  AND DOESN'T WORK AT ALL WITH MOST GCC VERSIONS.

所以仅使用SSE2,我们已经在两个额外指令中完成了条件MINPS(ORPS和MOVAPS,除非循环展开允许MOVAPS消失)。

没有SSE4.1 BLENDVPS的替代品是ANDPS / ANDNPS / ORPS混合,加上额外的MOVAPS。无论如何,ORPS比BLENDVPS更有效(在大多数CPU上它都是2 uops)。

答案 1 :(得分:2)

彼得·科德斯的回答很棒,我只是觉得我会用一些较短的逐点回答:

  
      
  • x86上的标量无分支minmax指令是什么?这是一系列指令吗?
  •   

我指的是minss / minsd。甚至其他没有这些指令的架构也应该能够通过条件移动无分支地完成这项工作。

  
      
  • 假设它将被应用,或者如何调用它是否安全?
  •   

gccclang会优化(a < b) ? a : bminss / minsd,所以我不打算使用内在函数。但是不能和其他编译器说话。

  
      
  • 关于min / max的无分支问题是否有意义?根据我的理解,对于光线跟踪器和/或其他视觉软件,给定光线盒交叉例程,分支预测器没有可靠的模式来拾取,因此消除分支确实有意义。我是对的吗?
  •   

单个a < b测试几乎完全无法预测,因此避免对这些测试进行分支非常重要。像if (ray.dir.x != 0.0)这样的测试是非常可预测的,因此避免使用这些分支并不重要,但它会缩小代码大小并使其更易于矢量化。最重要的部分可能是删除了分歧。

  
      
  • 最重要的是,所讨论的算法是围绕(+/-)INFINITY进行比较而建立的。这是可靠的w.r.t我们正在讨论的(未知)指令和浮点标准吗?
  •   

是的,minss / minsd的行为与(a < b) ? a : b完全相同,包括对无限性和NaN的处理。

另外,我在你引用的那篇文章中写了followup post,更详细地讨论了NaNs和min / max。

相关问题