【问题标题】:Why __m256 instead of 'float' gives more than x8 performance?为什么 __m256 而不是 'float' 提供的性能超过 x8?
【发布时间】:2019-06-21 21:41:57
【问题描述】:

为什么我通过使用__m256 数据类型获得了如此巨大的加速(x16 倍)? 一次处理 8 个浮点数,所以我希望只能看到 x8 加速?

我的 CPU 是 4 核 Devil Canyon i7(具有超线程) 在发布模式下使用 Visual Studio 2017 进行编译 - O2 优化已打开。

快速版本在 400x400 矩阵上消耗 0.000151 秒:

//make this matrix only keep the signs of its entries
inline void to_signs() {

    __m256 *i = reinterpret_cast<__m256*>(_arrays);
    __m256 *end = reinterpret_cast<__m256*>(_arrays + arraysSize());

    __m256 maskPlus = _mm256_set1_ps(1.f);
    __m256 maskMin =  _mm256_set1_ps(-1.f);

    //process the main portion of the array.  NOTICE: size might not be divisible by 8:
    while(true){
        ++i;
        if(i > end){  break; }

        __m256 *prev_i = i-1;
        *prev_i = _mm256_min_ps(*prev_i, maskPlus);
        *prev_i = _mm256_max_ps(*prev_i, maskMin);
    }

    //process the few remaining numbers, at the end of the array:
    i--;
    for(float *j=(float*)i; j<_arrays+arraysSize(); ++j){
        //taken from here:http://www.musicdsp.org/showone.php?id=249
        // mask sign bit in f, set it in r if necessary:
        float r = 1.0f;
        (int&)r |= ((int&)(*j) & 0x80000000);//according to author, can end up either -1 or 1 if zero.
        *j = r;
    }
}

旧版本,运行时间为 0.002416 秒:

inline void to_signs_slow() {
    size_t size = arraysSize();

    for (size_t i = 0; i<size; ++i) {
        //taken from here:http://www.musicdsp.org/showone.php?id=249
        // mask sign bit in f, set it in r if necessary:

        float r = 1.0f;
        (int&)r |= ((int&)_arrays[i] & 0x80000000);//according to author, can end up either -1 or 1 if zero.
        _arrays[i] = r;
    }
}

是不是偷偷用了2核,所以一旦我开始使用多线程这个好处就消失了?

编辑:

在较大的矩阵上,大小为 (10e6)x(4e4) 我平均得到 3 秒和 14 秒。所以只是 x4 加速,甚至不是 x8 This is probably due to memory bandwidth, and things not fitting in cache

不过,我的问题是关于令人愉快的 x16 加速惊喜 :)

【问题讨论】:

  • 恶魔峡谷处理器? Perhaps the answer is SATAN?
  • (int&amp;)r |= [...] 真的有效吗?它会将float&amp; 转换为int&amp; 吗?至少它看起来像严格的别名违规。
  • 如果end 是一个过去的数组指针(似乎是),那么if(i &gt; end){ break; } 是真的i 需要是过去的两个- 不存在的末端指针(试图得到一个是UB)。所以i &gt; end 可以安全地优化为false,循环可能没有中断条件。如果_mm256_and_psnoexcept,整个循环就是UB,意味着整个函数就是UB。上面的例子可以简单地优化出来。你看过生成的程序集吗?
  • 你能测试更大的矩阵(达到大约 1 秒的时间)吗?这样的微基准根本不可靠。或者一次运行至少运行 1000 次。

标签: c++ visual-c++ compiler-optimization sse intrinsics


【解决方案1】:

您的标量版本看起来很糟糕(带有用于类型双关的引用转换),并且可能编译为效率非常低的 asm,这比将每个 32 位元素复制到 1.0f 的位模式中要慢得多。这应该只需要一个整数 AND 和一个 OR 来进行标量(如果 MSVC 未能为您自动矢量化),但如果编译器将它复制到 XMM 寄存器或其他东西,我不会感到惊讶。


不过,您的第一个手动矢量化版本甚至没有做同样的工作,它只是掩盖了所有非符号位,留下-0.0f+0.0f。所以它将编译为一个vandps ymm0, ymm7, [rdi] 和一个带有vmovups [rdi], ymm0 的SIMD 存储,加上一些循环开销。

并不是说添加_mm256_or_psset1(1.0f) 会减慢速度,您仍然会成为缓存带宽或每时钟1 次存储吞吐量的瓶颈。


然后您将其编辑为钳制在-1.0f .. +1.0f 范围内的版本,使幅度小于 1.0 的输入保持不变。这不会比两个按位操作慢,除了 Haswell(魔鬼峡谷)仅在端口 5 上运行 FP 布尔值,而在端口 0 或端口 1 上运行实际的 FP 内容。

特别是如果你没有对你的浮点数做任何其他事情,你实际上会想要使用 _si256 内在函数来对它们使用 AVX2 整数指令,以提高 Haswell 的速度。 (但如果没有 AVX2,您的代码将无法运行。)

在 Skylake 和更新版本上,FP 布尔值可以使用所有 3 个向量 ALU 端口。 (https://agner.org/optimize/ 用于指令表和 uarch 指南。)

您的代码应该类似于:

// outside the loop if you want
const __m256i ones = _mm256_castps_si256(_mm256_set1_ps(1.0f));

for (something ; p += whatever) {
    __m256i floats = _mm256_load_si256( (const __m256i*)p );
    __m256i signs = _mm256_and_si256(floats,  _mm256_set1_epi32(0x80000000));
    __m256i applied = _mm256_or_si256(signs, ones);
    _mm256_store_si256((__m256i*)p, applied);

}

【讨论】:

  • @Kari:如果您发布了可以实际编译的代码minimal reproducible example,则很容易查看 MSVC 将在 Godbolt 编译器资源管理器godbolt.org 上为其创建的 asm。例如将 args 传递给函数而不是让它引用全局变量或其他函数,这就是函数采用 args 的全部意义。
猜你喜欢
  • 1970-01-01
  • 2021-10-29
  • 1970-01-01
  • 2012-05-28
  • 1970-01-01
  • 2023-02-14
  • 1970-01-01
  • 2023-01-16
  • 1970-01-01
相关资源
最近更新 更多