代码之家  ›  专栏  ›  技术社区  ›  Kari

为什么用M256而不是“float”来提供X8以上的性能?

  •  3
  • Kari  · 技术社区  · 6 年前

    为什么我会通过使用 __m256 datatype? 一次处理8个浮点,所以我只希望看到X8加速?

    我的CPU是4核恶魔峡谷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加速惊喜:)

    1 回复  |  直到 6 年前
        1
  •  4
  •   Peter Cordes    6 年前

    您的标量版本看起来很糟糕(使用类型punning的引用强制转换),并且可能编译为效率非常低的ASM,这比将每个32位元素复制到位模式慢得多。 1.0f . 这只需要一个整数和一个或来做标量(如果MSVC不能为您自动向量化),但是如果编译器将它复制到一个xmm寄存器或其他地方,我不会感到惊讶。


    你的第一个手动矢量化版本甚至没有做同样的工作,不过,它只是屏蔽了所有要离开的非符号位。 -0.0f +0.0f . 所以它会编译成一个 vandps ymm0, ymm7, [rdi] 和一个模拟人生商店 vmovups [rdi], ymm0 ,加上一些循环开销。

    不是那个加法 _mm256_or_ps 具有 set1(1.0f) 会减慢任何速度,您仍然会在缓存带宽或每时钟一次的存储吞吐量上遇到瓶颈。


    然后你把它编辑成一个版本 -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);
    
    }