【问题标题】:Horizontal minimum and position in SSE for unsigned 32-bit integersSSE 中无符号 32 位整数的水平最小值和位置
【发布时间】:2015-04-06 00:19:19
【问题描述】:

我正在寻找一种方法来找到 SSE 中无符号 32 位整数的最小值及其位置(类似于 _mm_minpos_epu16)。我知道我可以通过一系列 _mm_min_epu32 和 shuffles/shifts 找到最小值,但这并没有让我得到这个位置。

有人有什么很酷的方法吗?

【问题讨论】:

  • 您想要将位置作为索引值(如_mm_minpos_epu16 那样)还是掩码可以(最小元素设置为-1,所有其他元素设置为0)?
  • 请问您为什么要这样做?我无法想象为什么您要在循环中的每次迭代中都这样做。为什么这很关键?我想如果我知道英特尔为什么首先创建 _mm_minpos_epu16 会有所帮助。

标签: performance algorithm optimization sse simd


【解决方案1】:

一般来说,如果一个人在 SIMD 中使用水平运算符,这很好地表明 SIMD 没有得到最佳使用。但是,水平操作在循环结束时很好,在这种情况下我会这样做

int result[4] __attribute__((aligned(16)));
_mm_store_si128((__m128i *) result, v);
for(int i=0; i<4; i++) if(result[i]<min) { min = result[i]; index = i; }

不过,这里有一些使用 SSE 的解决方案。我不知道它们是否比上面的代码更好。

第一个解决方案是 Paul R 答案的变体。

vmin = _mm_min_epu32(vmin, _mm_alignr_epi8(vmin, vmin, 4));
vmin = _mm_min_epu32(vmin, _mm_alignr_epi8(vmin, vmin, 8));
__m128i vmask = _mm_cmpeq_epi32(v, vmin);
vmask = _mm_xor_si128(vmask, _mm_set1_epi32(-1));
__m128i vpos = _mm_minpos_epu16(vmask);

vpos 中的第二个 16 位字包含两倍的位置。

这是使用_mm_minpos_epu16 的另一个变体。它首先找到最小的高 16 位,然后屏蔽掉不在最小 16 位中的值(通过将它们设置为高),然后找到低 16 位的最小值以及位置。

__m128i mask1 = _mm_setr_epi8(0x0,0x1,0x4,0x5, 0x8,0x9,0xc,0xd, 0x0,0x1,0x4,0x5,  0x8,0x9,0xc,0xd);
__m128i mask2 = _mm_setr_epi8(0x2,0x3,0x6,0x7, 0xa,0xb,0xe,0xf, 0x2,0x3,0x6,0x7,  0xa,0xb,0xe,0xf);
__m128i mask3 = _mm_set1_epi32(0x01000100);

掩码是常量,因此可以在编译时或循环外计算。

__m128i lo = _mm_shuffle_epi8(v,mask1);            //lower 16-bits
__m128i hi = _mm_shuffle_epi8(v,mask2);            //upper 16-bits
__m128i t1 = _mm_minpos_epu16(hi);                 //upper 16-bits min
__m128i t2 = _mm_shuffle_epi8(t1, mask3);          //broadcast upper min
__m128i t3 = _mm_cmpeq_epi32(t2,hi);               //select equal
__m128i t4 = _mm_xor_si128(t3, _mm_set1_epi32(-1));//invert
__m128i t5 = _mm_or_si128(lo,t4);                   
__m128i t6 = _mm_minpos_epu16(t5);                 //lower 16-bits hi and position

最小值的高 16 位在 t1 的前 16 位中,最小值的低 16 位在 t6 的前 16 位中。位置在t6的第二个16位中。

【讨论】:

  • 倒置掩码然后使用 _mm_minpos_epu16 获得 2 倍索引的好主意!
  • @PaulR,谢谢。是的,太糟糕了 SSE 没有 !=&gt; 用于未签名。 XOP 和 AVX512 都有。
  • 很抱歉提到它,但是这个伟大的代码中有一个印刷错误!应该不是 __m128i t3 = _mm_cmpeq_epi32(t2,hi);但是 __m128i t3 = _mm_cmpeq_epi16(t2,hi);除此之外,它就像一个魅力,谢谢!顺便说一句,它对于 AVX 的使用非常有用 - 它正好包含 8 个 32 位,因此在相应的 shuffle 之后,我们得到 8 个上半部分和下半部分进行处理。
【解决方案2】:

可能有一个更聪明的方法,但现在这里是一种蛮力方法:

#include <stdio.h>
#include <smmintrin.h> // SSE4.1

int main(void)
{
    __m128i v = _mm_setr_epi32(42, 1, 43, 2);

    printf("v     = %vlu\n", v);

    __m128i vmin = v;

    vmin = _mm_min_epu32(vmin, _mm_alignr_epi8(vmin, vmin, 4));
    vmin = _mm_min_epu32(vmin, _mm_alignr_epi8(vmin, vmin, 8));
                                                   // get min value in all elements of vmin

    printf("vmin  = %vlu\n", vmin);

    __m128i vmask = _mm_cmpeq_epi32(v, vmin);      // set min element(s) in mask to -1,
                                                   // all others to 0 [1]

    printf("vmask = %vld\n", vmask);

    int16_t mask = _mm_movemask_epi8(vmask);       // get mask as scalar [2]

    printf("mask  = %#x\n", mask);

    int pos = __builtin_ctz(mask) >> 2;            // convert scalar mask to index [3]

    printf("pos   = %d\n", pos);

    return 0;
}

如果您可以使用设置在最小元素位置的掩码,那么您可以在 [1] 处停止,否则继续 [3] 以获取(最不重要的)的索引) 最小元素。

还要注意__builtin_ctz 是一个 gcc 特定的内在函数(尽管它也可以在其他 gcc 兼容的编译器中找到)。如果您使用的是 MSVC,则需要使用等效的 Microsoft 内在函数 (_BitScanForward)。

【讨论】:

  • 你打败了我 :-) 我可能有使用 minpos 的不同方法,但我不确定。最有效的解决方案可能是将结果存储到一个数组中并遍历四个元素。
  • 嘿——你得早点上 StackOverflow ! ;-) 我认为最好的解决方案取决于结果的首选格式是什么 - 如果向量最小值和掩码就足够了,那么使用上述方法只需 5 条指令,但如果需要实际索引,那么我怀疑可能有更好的方法。
  • 你能想到一个关键的案例吗?为什么_mm_minpos_epu16 还会存在?
  • 我认为它可能用于 MPEG 之类的东西,也许用于运动补偿,但视频并不是我真正的领域。
  • 是的,我需要实际的索引值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-03-09
  • 1970-01-01
  • 2013-10-05
  • 2011-01-31
  • 2019-05-30
  • 2012-07-27
  • 2011-06-02
相关资源
最近更新 更多