【问题标题】:x86 max/min asm instructions?x86 最大/最小 asm 指令?
【发布时间】:2010-12-30 12:00:34
【问题描述】:

是否有任何 asm 指令可以加快 Core i7 架构上双精度/整数向量的最小值/最大值的计算?

更新:

没想到答案这么丰富,谢谢。 所以我看到 max/min 可以在没有分支的情况下完成。 我有一个子问题:

有没有一种有效的方法来获取数组中最大双精度数的索引?

【问题讨论】:

  • 宿主语言是什么?如果是 c/c++ 我就不会太担心了。
  • 最大约 300 个双打位于大型程序的最内层循环中。 85% 的时间花在 8000 行代码中的大约 10 行上。正因为如此,宿主语言并不重要。但是是的,它是 C++
  • 相关:What is the instruction that gives branchless FP min and max on x86? 有更多关于 MINSS / MAXSS / MINSD / MAXSD 的详细信息,包括它们的 NaN 行为。

标签: assembly x86 intrinsics


【解决方案1】:

SSE4 有 PMAXSDPMAXUD 用于 32 位有符号/无符号整数,这可能很有用。

SSE2 有MAXPDMAXSD,它们在成对的双精度之间进行比较,因此您可以按照 n/2-1 MAXPD 和一个 MAXSD 来获得 n 向量的最大值,并使用通常的交错加载和操作。

上面有 MIN 等价物。

对于双重情况​​,您在汇编程序中的表现可能不会比在 SSE 模式下的半体面 C++ 编译器做得更好:

peregrino:$ g++ -O3 src/min_max.cpp -o bin/min_max
peregrino:$ g++ -O3 -msse4 -mfpmath=sse src/min_max.cpp -o bin/min_max_sse
peregrino:$ time bin/min_max
0,40

real    0m0.874s
user    0m0.796s
sys 0m0.004s
peregrino:$ time bin/min_max_sse 
0,40

real    0m0.457s
user    0m0.404s
sys 0m0.000s

其中 min_max 使用简单循环计算 500 个数组的最小值和最大值 100,000 次:

bool min_max ( double array[], size_t len, double& min, double& max )
{
    double min_value = array [ 0 ];
    double max_value = array [ 0 ];

    for ( size_t index = 1; index < len; ++index ) {
        if ( array [ index ] < min_value ) min_value = array [ index ];
        if ( array [ index ] > max_value ) max_value = array [ index ];
    }

    min = min_value;
    max = max_value;
}

针对第二部分,从最大操作中删除分支的传统优化是比较值,将标志作为单个位(给出 0 或 1),减一(给出 0 或 0xffff_ffff)和 'and ' 它与两个可能结果的异或,所以你得到相当于( a &gt; best ? ( current_index ^ best_index ) : 0 ) ^ best_index )。我怀疑是否有一种简单的 SSE 方法可以做到这一点,仅仅是因为 SSE 倾向于对打包值而不是标记值进行操作;有一些水平索引操作,所以你可以尝试找到最大值,然后从原始向量中的所有元素中减去它,然后收集符号位,零符号位将对应于最大值的索引,但这可能除非您使用短裤或字节,否则不会有任何改进。

【讨论】:

  • 您只需要 log2(vector_length) shuffle + MAXPS/MAXPD 操作,而不是 VL/2,即可获得单个 SIMD 向量的水平最大值。和a horizontal sum的思路基本一样:每次都缩小一半。 (或者将结果广播留给每个元素,交换高/低)。
  • 如果您没有内存瓶颈,使用多个累加器展开应该可以提供超过 2 倍的加速。 (MAXPD 有 3 或 4 个周期的延迟,但每个周期的吞吐量为 1,因此您需要编译器发出使用多个向量并在数组末尾组合它们的 asm。) clang 倾向于在自动执行此操作时执行此操作矢量化,但 gcc 通常仍然不会。
【解决方案2】:

SSE 的 MAXPS 和 MINPS 都对压缩的单精度浮点数进行运算。 PMAXSW、PMINSW、PMAXUB 和 PMINUB 都对压缩的 8 位字(有符号或无符号)进行操作。请注意,它们按元素比较两个输入 SSE 寄存器或地址位置,并将结果存储到 SSE 寄存器或内存位置。

SSE2 版本的 MAXPS 和 MINPS 应该适用于双精度浮点数。

您使用什么编译器和优化标志?如果您的目标支持,gcc 4.0 及更高版本应自动矢量化操作,早期版本可能需要特定标志。

【讨论】:

    【解决方案3】:

    如果您使用 Intel 的 IPP 库,您可以使用向量 statistical functions 来计算向量最小值/最大值(除其他外)

    【讨论】:

      【解决方案4】:

      回答您的第二个问题:在大多数平台上,有些库已经包含此操作(以及大多数其他简单向量操作)的优化实现。 使用它们

      • 在 OS X 上,Accelerate.framework 中有 vDSP_maxviD( )cblas_idamax( )
      • 英特尔编译器包括 IPP 和 MKL 库,它们具有高性能实现,包括 cblas_idamax( )
      • 大多数 Linux 系统在 BLAS 库中都有 cblas_idamax( ),根据其出处可能会或可能不会进行良好调整;关心性能的用户通常会有一个很好的实现(或者可以被说服安装一个)
      • 如果一切都失败了,您可以使用 ATLAS(自动调谐线性代数软件)在目标平台上获得不错的性能实现

      【讨论】:

        【解决方案5】:

        更新:我刚刚意识到你在第 2 部分中说的是“数组”,而不是“向量”。无论如何我都会把它留在这里,以防它有用。


        re:第二部分:查找 SSE 向量中最大/最小元素的索引:

        • 做一个水平最大值。对于 2 个 double 元素的 128b 向量,只需一个 shufpd + maxpd 即可将结果广播留给两个元素。

          对于其他情况,当然会采取更多的步骤。有关想法,请参阅 Fastest way to do horizontal float vector sum on x86,将 addps 替换为 maxpsminps。 (但请注意,16 位整数是特殊的,因为您可以使用 SSE4 phminposuw。对于最大值,从 255 中减去)

        • 在向量原始向量和每个元素都是最大值的向量之间进行压缩比较。

          pcmpeqq 整数位模式或通常的cmpeqpd 都适用于double 情况)。

        • int _mm_movemask_pd (__m128d a) (movmskpd) 以整数位图的形式获取比较结果。
        • 位扫描(bsf)它的(第一个)匹配:index = _bit_scan_forward(cmpmask)。如果您使用整数比较,则 cmpmask = 0 是不可能的(因为至少有一个元素会匹配,即使它们是 NaN)。

        这应该只编译为 6 条指令(包括 movapd)。是的,刚刚在the Godbolt compiler explorer 上进行了检查,SSE 确实如此。

        #include <immintrin.h>
        #include <x86intrin.h>
        
        int maxpos(__m128d v) {
          __m128d swapped = _mm_shuffle_pd(v,v, 1);
          __m128d maxbcast = _mm_max_pd(swapped, v);
          __m128d cmp = _mm_cmpeq_pd(maxbcast, v);
          int cmpmask = _mm_movemask_pd(cmp);
          return _bit_scan_forward(cmpmask);
        }
        

        请注意_mm_max_pd is not commutative with NaN inputs。如果 NaN 是可能的,并且您不关心 Intel Nehalem 的性能,您可以考虑使用 _mm_cmpeq_epi64 来比较位模式。不过,从 float 到 vec-int 的旁路延迟是 Nehalem 的一个问题。

        NaN != IEEE 浮点中的 NaN,因此在全 NaN 情况下,_mm_cmpeq_pd 结果掩码可能全为零。

        在 2 元素的情况下,您可以做的另一件事总是得到 0 或 1 是用cmpmask &gt;&gt; 1 替换位扫描。 (bsf 在 input = 全零时很奇怪)。

        【讨论】:

          【解决方案6】:

          在回答您的第二个问题时,您可能值得考虑一下您收集和存储这些数据的方式。

          您可以将数据存储在始终保持数据排序的 B 树中,只需要对数比较操作。

          那么你随时都知道最大值在哪里。

          http://en.wikipedia.org/wiki/B_tree

          【讨论】:

          猜你喜欢
          • 2013-01-19
          • 2019-04-20
          • 1970-01-01
          • 1970-01-01
          • 2023-03-30
          • 1970-01-01
          • 2019-07-23
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多