【问题标题】:Minimum of 4 SP values in __m128__m128 中至少有 4 个 SP 值
【发布时间】:2013-07-14 10:17:59
【问题描述】:

假设有一个 __m128 变量保存 4 个 SP 值,并且您想要最小的一个,是否有可用的内在函数,或者除了值之间的朴素线性比较之外的任何东西?

知道我的解决方案如下(假设输入__m128变量为x):

x = _mm_min_ps(x, (__m128)_mm_srli_si128((__m128i)x, 4));
min = _mm_min_ss(x, (__m128)_mm_srli_si128((__m128i)x, 8))[0];

这很可怕,但它确实有效(顺便说一句,除了__m128 类型之外,有没有类似_mm_srli_si128 的东西?)

【问题讨论】:

标签: c sse simd


【解决方案1】:

没有单一的指令/内在,但您可以通过两次随机播放和两分钟来完成:

__m128 _mm_hmin_ps(__m128 v)
{
    v = _mm_min_ps(v, _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 1, 0, 3)));
    v = _mm_min_ps(v, _mm_shuffle_ps(v, v, _MM_SHUFFLE(1, 0, 3, 2)));
    return v;
}

输出向量将包含输入向量中所有元素的最小值,并在整个输出向量中复制。

【讨论】:

  • 你确定这是对的吗?我用这个输入向量(从v[0]v[3]0.109375 0.096875 0.093750 0.096875 进行了测试,这是输出:0.096875 0.096875 0.093750 0.093750。复制是什么意思?
  • 抱歉 - 我搞砸了置换常量 - 现在已修复并经过测试。输出向量的所有 4 个元素都等于输入向量的最小元素。
  • 如果您不需要广播结果,您可以让编译器使用_mm_movehl_ps (movhlps)) 和可选的 SSE3 @987654330 使用 SSE1 或 SSE3 保存一个或两个 movaps @。见Fastest way to do horizontal float vector sum on x86。 (只需将add 替换为min;改组是一样的。)
【解决方案2】:

Paul R 的回答很棒! (@Paul R - 如果你读到了,谢谢!)我只是想解释一下它对像我这样的 SSE 新手的实际工作方式。当然 可能我哪里有错,欢迎大家指正!

_mm_shuffle_ps 是如何工作的?

首先,SSE 寄存器的索引与您的预期相反,如下所示:

[6, 9, 8, 5] // values
 3  2  1  0  // indexes

这种索引顺序使向量左移将数据从低索引移动到高索引,就像左移整数中的位一样。最重要的元素在左边。


_mm_shuffle_ps可以混合两个寄存器的内容:

// __m128 a : (a3, a2, a1, a0)
// __m128 b : (b3, b2, b1, b0)
__m128 two_from_a_and_two_from_b = _mm_shuffle_ps(b, a, _MM_SHUFFLE(3, 2,   1, 0));
//                                                                  ^  ^    ^  ^ 
//                                            indexes into second operand    indexes into first operand
// two_from_a_and_two_from_b : (a3, a2, b1, b0)

在这里,我们只想打乱一个寄存器的值,而不是两个。我们可以通过传递 v 作为两个参数来做到这一点,就像这样(你可以在 Paul R 的函数中看到这一点):

// __m128 v : (v3, v2, v1, v0)
__m128 v_rotated_left_by_1 = _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 1, 0, 3));
// v_rotated_left_by_1 : (v2, v1, v0, v3) // i.e. move all elements left by 1 with wraparound

为了便于阅读,我将把它封装在一个宏中:

#define mm_shuffle_one(v, pattern)  _mm_shuffle_ps(v, v, pattern)

(它不能是函数,因为 _mm_shuffle_pspattern 参数在编译时必须是常量。)

这是实际函数的略微修改版本 - 我添加了中间名称以提高可读性,因为编译器无论如何都会优化它们:

inline __m128 _mm_hmin_ps(__m128 v){
    __m128  v_rotated_left_by_1 = mm_shuffle_one(v,  _MM_SHUFFLE(2, 1, 0, 3));
    __m128 v2 = _mm_min_ps(v,   v_rotated_left_by_1);

    __m128 v2_rotated_left_by_2 = mm_shuffle_one(v2, _MM_SHUFFLE(1, 0, 3, 2));
    __m128 v3 = _mm_min_ps(v2, v2_rotated_left_by_2);

    return v3;
}

为什么要像我们现在这样洗牌?我们如何通过两个min 操作找到四个元素中最小的一个?

我在了解如何通过两个矢量化的min 操作来实现min 4 个浮点数时遇到了一些麻烦,但是当我手动跟踪哪些值是min'd 一起时,我明白了,一步一步。 (虽然自己做可能比阅读更有趣)

假设我们有v

[7,6,9,5] v

首先,我们minvv_rotated_left_by_1的值:

[7,6,9,5] v
 3 2 1 0  // (just the indices of the elements)
[6,9,5,7] v_rotated_left_by_1
 2 1 0 3  // (the indexes refer to v, and we rotated it left by 1, so the indices are shifted)
--------- min
[6,6,5,5] v2
 3 2 1 0 // (explained
 2 1 0 3 //  below    )

v2 的元素下的每一列跟踪v 的哪些索引是min 一起获得该元素的。 所以,按列从左到右:

v2[3] == 6 == min(v[3], v[2])
v2[2] == 6 == min(v[2], v[1])
v2[1] == 5 == min(v[1], v[0])
v2[0] == 5 == min(v[0], v[3])

现在是第二个min

[6,6,5,5] v2
 3 2 1 0
 2 1 0 3
[5,5,6,6] v2_rotated_left_by_2
 1 0 3 2
 0 3 2 1
--------- min
[5,5,5,5] v3
 3 2 1 0
 2 1 0 3
 1 0 3 2
 0 3 2 1

瞧! v3 下的每一列都包含 (3,2,1,0) - v3 的每个元素与 v 的所有元素都是 mind - 所以每个元素都包含整个向量 v 的最小值。

使用函数后,可以用float _mm_cvtss_f32(__m128)提取最小值:

__m128 min_vector = _mm_hmin_ps(my_vector);
float minval = _mm_cvtss_f32(min_vector);

***

这只是一个切线的想法,但我发现有趣的是,这种方法可以扩展到任意长度的序列,在每一步将上一步的结果旋转1, 2, 4, 8, ... 2**ceil(log2(len(v)))(我认为)。 从理论上讲,这很酷 - 如果您可以同时按元素比较两个序列,您可以在对数时间内找到序列的最小/最大1

1 这扩展到所有水平折叠/减少,例如 sum。同样的洗牌,不同的垂直操作。

但是,AVX(256 位向量)使 128 位边界变得特别,并且更难跨越。如果您只想要一个标量结果,请提取高半部分,以便每一步都将矢量宽度缩小一半。 (就像在 Fastest way to do horizontal float vector sum on x86 中一样,对于 128 位向量,它比 2x shufps 更有效的洗牌,在没有 AVX 的情况下编译时避免了一些 movaps 指令。)

但是,如果您希望像@PaulR 的回答一样将结果广播到每个元素,您需要进行通道内随机播放(即在每个通道的 4 个元素内旋转),然后交换一半,或旋转 128 位通道.

【讨论】:

  • 任意长度的向量:对于 AVX / AVX512,通常最好将其缩小到 128 位向量。因此,提取高半部分,然后提取高/低半部分(如Horizontal sum of 32-bit floats in 256-bit AVX vector,但使用 min 而不是 add)。或者,如果您真的希望在最后广播结果而不是缩小到标量,您仍然希望最大限度地减少车道交叉洗牌。例如vperm2f128 交换高/低,然后 vminps 和通道内随机播放。
  • 但对于没有 AVX 的 128 位情况,请参阅 Fastest way to do horizontal float vector sum on x86 以了解将编译器保存为 movaps 的随机播放。 (例如,使用_mm_movehl_ps (movhlps))。此外,将向量的低元素作为标量浮点数的内在函数是_mm_cvtss_f32_mm_store_ss 可能会优化掉,但获取本地地址过于复杂。
  • 感谢您的评论。那不是我真正在说的,我改变了我的答案以反映这一点,但仍然很高兴知道!虽然我的笔记本电脑不支持 SSE 以外的任何东西,所以无论如何我都不会测试...
  • 既然您说“欢迎任何更正”,我进行了一些编辑以改进/澄清您的答案。随意重新编辑,用你自己的话来说,或删除任何你认为过于切题的东西。
  • 谢谢!很高兴看到关于_mm_store_ss 的更正——这只是我发现的第一个方法。顺便说一句,如果我将结果写入数组,你会推荐哪一个?
猜你喜欢
  • 2019-05-28
  • 2022-01-13
  • 1970-01-01
  • 1970-01-01
  • 2015-09-22
  • 2014-07-15
  • 2018-10-06
  • 2013-10-04
相关资源
最近更新 更多