【问题标题】:Efficient min() function in SSESSE 中的高效 min() 函数
【发布时间】:2016-02-10 05:17:05
【问题描述】:

我有以下循环,它取数组中每个条目的平方根:

#include <mmintrin.h>

float array[SIZE];
for (int i = 0; i < SIZE; i += 4)
{
    __m128 fourFloats, fourRoots;
    fourFloats = _mm_load_ps(&array[i]);
    fourRoots = _mm_sqrt_ps(fourFloats);
    float results[4];
    _mm_store_ps(results, fourRoots);
    // This is bottleneck
    array[i]   = results[0] > 63.0F ? 63.0F : floor(results[0]);
    array[i+1] = results[1] > 63.0F ? 63.0F : floor(results[1]);
    array[i+2] = results[2] > 63.0F ? 63.0F : floor(results[2]);
    array[i+3] = results[3] > 63.0F ? 63.0F : floor(results[3]);
    // This is slower
//  array[i] = (int) std::min(floor(results[0]), 63.0F);
}

根据我的分析器 (Zoom),平方根不会花费大量时间,但结果的四个剪辑中的每一个都需要大约 20% 的时间,即使启用了 -O2 优化也是如此。有没有更有效的方法来实现循环?请注意,_mm_store_ps() 会被 gcc 优化掉。

我尝试了平方根的优化表查找,因为 97% 的输入 array 值低于 512,但这并没有帮助。请注意,对于我的完整应用程序(一个持续运行的图像识别应用程序)而言,此例程占用的总处理器时间不到四分之一。

【问题讨论】:

  • 您是否尝试在fourRoots 上使用_mm_floor_ps,然后再将其存储到results 中,然后再制作您拥有的这些条件内容?
  • 顺便说一句,有 SSE 命令一次处理 8 个浮点数
  • @ixSci 我没有意识到有这样的功能!我现在查了一下,SSE4 是 2007 年左右的,因为我们的目标是 i7 左右的设备,所以我对使用这个指令没有任何疑虑。
  • 在下面更正了我的答案
  • 您应该使用gcc -O3 来启用自动矢量化。转换为int 并且返回很慢,尤其是,我并不感到惊讶。如果您使用的是标量,而不是使用_mm_cvttps_epi32(额外的t 表示截断。)如果您希望结果最终为float,那么使用floor_ps 并夹到63.0f 与min_ps。这需要 SSE4.1,因此您需要第二代 core2。 (或 AVX 用于 256b 向量,但这需要 Intel Sandybridge / AMD Bulldozer。第一代 i7(Nehalem/Westmere)已经过时但并未消失,甚至还没有完全过时。)

标签: c++ sse


【解决方案1】:

MAXPSMINPS

__m128d _mm_max_ps(__m128d a, __m128d b);

对第一个源操作数和第二个源操作数中的压缩单精度浮点值执行 SIMD 比较,并将每对值的最大值返回给目标操作数。

__m128d _mm_min_ps(__m128d a, __m128d b);

对第一个源操作数和第二个源操作数中的压缩单精度浮点值执行 SIMD 比较,并将每对值的最小值返回给目标操作数。

使用具有四个 63.0f 值的 XMM 寄存器作为第二个操作数。

【讨论】:

  • _mm_min_ps() 是我正在寻找的内容,因此结合这两个回复,我已经将总运行时间减少了 25%,而这个函数的贡献从大约 24% 下降到了 7%。谢谢。
  • @KenY-N,如何也使用__m256 寄存器(和相应的命令)?我认为它会进一步改善您的体验。
  • @ixSci 这又降低了几个百分点,在将-mavx 添加到我的编译行之后,由于SIZE 是40,它还展开了内部循环。我想__m512 会更好。
【解决方案2】:

鉴于您可以使用非常现代的 CPU,我将从以下方面入手:

float array[SIZE];
for(int i = 0; i < SIZE; i += 8)
{
    __m256 eightFloats, eightRoots;
    eightFloats = _mm256_load_ps(&array[i]);
    eightRoots = _mm256_sqrt_ps(eightFloats);
    float results[8];
    eightRoots = _mm256_floor_ps(eightRoots);
    _mm256_store_ps(results, eightRoots);
    ...
}

如果允许最高级的 SIMD 指令,甚至会选择 512 版本。

【讨论】:

  • 没有 AVX512 硬件,除了 Intel 的计算卡 (Xeon Phi)。 Skylake-E 核心尚未推出。当前任何“skylake xeon”芯片仍然是没有 AVX512 的桌面内核。
【解决方案3】:

总结这两个答案,这是我最终决定满足我的全部要求的代码,array[i] = std::min(floor(sqrt(array[i])), (float) 0x3f);

float array[SIZE];
const float clipValue = (float) 0x3f;
const float clipArray[8] = {clipValue, clipValue, clipValue, clipValue,
                            clipValue, clipValue, clipValue, clipValue};
__m256 eightClips = _mm256_load_ps(clipArray);
for(int i = 0; i < SIZE; i += 8)
{
    __m256 eightFloats = _mm256_load_ps(&array[i]);
    __m256 eightRoots  = _mm256_sqrt_ps(eightFloats);
    __m256 eightFloors = _mm256_floor_ps(eightRoots);
    __m256 eightMins   = _mm256_min_ps(eightFloors, eightClips);
    _mm256_store_ps(&array[i], eightMins);
}

我的目标是垂直应用程序中的特定硬件,因此需要一个兼容 AVX 的处理器。

【讨论】:

  • 只需使用_mm256_set1_ps(63.0f)。如果clipArray 是本地的,您的编译器实际上可能会发出将clipValue 存储到堆栈8 次的代码!例如,请参阅my answer on vector constants。您可能想让您的编译器从单个静态常量 float 发出 vbroadcastss 负载,如果它还没有将 set1 变成那样。 Clang 应该,gcc 可能不会。 63.0f 有一个位模式,需要多条指令到 gen on the fly with shifts :/
  • @Peter Cordes:_mm256_set1_ps(63.0f) 的替代方案是 clipValuei32=_mm256_undefined_si256(); clipValuei32=_mm256_cmpeq_epi32(clipValuei32,clipValuei32); clipValuei32=_mm256_srli_epi32(clipValuei32,26); clipValue=_mm256_cvtepi32_ps(clipValuei32); ,如果缓存未命中是一个问题,它可能会更快。它编译成三个指令,每个指令在 Skylake 上的吞吐量只有 0.5 个周期。
  • @wim:哦,好点,整数 63 很容易生成。你最好的选择通常是从set1_epi32(-1) 开始,因为有些编译器在处理mm_undefined_*() 方面很糟糕。除了在 MSVC 上,您可以获得 256b 的全一常量,而不是 vpcmpeq。需要进一步测试以查看 MSVC 是否总是为 mm_undefined 插入 pxor 依赖中断(不需要,因为 vpcmpeq same,same 已经是 dep 中断)。
猜你喜欢
  • 1970-01-01
  • 2019-05-30
  • 1970-01-01
  • 2014-02-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-02
相关资源
最近更新 更多