4

我知道在没有先将所有 ymm 寄存器的上半部分清零的情况下从 AVX 指令切换到 SSE 指令的现有惩罚,但在我的机器(i7-3939K 3.2GHz)上的特殊情况下,似乎有一个非常即使我在 AVX 代码部分之前和之后明确使用 _mm256_zeroupper ,也会因为相反的方式(SSE 到 AVX)而受到很大的惩罚。

我已经编写了用于在 32768 个元素宽的 2 个缓冲区上转换 32 位浮点数和 32 位定点整数的函数。我将 SSE2 内在版本直接移植到 AVX 以一次执行 8 个元素,而不是 SSE 的 4 个,期望看到显着的性能提升,但不幸的是,相反的事情发生了。

所以,我有两个功能:

void ConvertPcm32FloatToPcm32Fixed(int32* outBuffer, const float* inBuffer, uint sampleCount, bool bUseAvx)
{
    const float fScale = (float)(1U<<31);

    if (bUseAvx)
    {
        _mm256_zeroupper();
        const __m256 vScale = _mm256_set1_ps(fScale);
        const __m256 vVolMax = _mm256_set1_ps(fScale-1);
        const __m256 vVolMin = _mm256_set1_ps(-fScale);

        for (uint i = 0; i < sampleCount; i+=8)
        {
            const __m256 vIn0 = _mm256_load_ps(inBuffer+i); // Aligned load
            const __m256 vVal0 = _mm256_mul_ps(vIn0, vScale);
            const __m256 vClamped0 = _mm256_min_ps( _mm256_max_ps(vVal0, vVolMin), vVolMax );
            const __m256i vFinal0 = _mm256_cvtps_epi32(vClamped0);
            _mm256_store_si256((__m256i*)(outBuffer+i), vFinal0); // Aligned store
        }
        _mm256_zeroupper();
    }
    else
    {
        const __m128 vScale = _mm_set1_ps(fScale);
        const __m128 vVolMax = _mm_set1_ps(fScale-1);
        const __m128 vVolMin = _mm_set1_ps(-fScale);

        for (uint i = 0; i < sampleCount; i+=4)
        {
            const __m128 vIn0 = _mm_load_ps(inBuffer+i); // Aligned load
            const __m128 vVal0 = _mm_mul_ps(vIn0, vScale);
            const __m128 vClamped0 = _mm_min_ps( _mm_max_ps(vVal0, vVolMin), vVolMax );
            const __m128i vFinal0 = _mm_cvtps_epi32(vClamped0);
            _mm_store_si128((__m128i*)(outBuffer+i), vFinal0); // Aligned store
        }
    }
}

void ConvertPcm32FixedToPcm32Float(float* outBuffer, const int32* inBuffer, uint sampleCount, bool bUseAvx)
{
    const float fScale = (float)(1U<<31);

    if (bUseAvx)
    {
        _mm256_zeroupper();
        const __m256 vScale = _mm256_set1_ps(1/fScale);

        for (uint i = 0; i < sampleCount; i+=8)
        {
            __m256i vIn0 = _mm256_load_si256(reinterpret_cast<const __m256i*>(inBuffer+i)); // Aligned load
            __m256 vVal0 = _mm256_cvtepi32_ps(vIn0);
            vVal0 = _mm256_mul_ps(vVal0, vScale);
            _mm256_store_ps(outBuffer+i, vVal0); // Aligned store
        }
        _mm256_zeroupper();
    }
    else
    {
        const __m128 vScale = _mm_set1_ps(1/fScale);

        for (uint i = 0; i < sampleCount; i+=4)
        {
            __m128i vIn0 = _mm_load_si128(reinterpret_cast<const __m128i*>(inBuffer+i)); // Aligned load
            __m128 vVal0 = _mm_cvtepi32_ps(vIn0);
            vVal0 = _mm_mul_ps(vVal0, vScale);
            _mm_store_ps(outBuffer+i, vVal0); // Aligned store
        }
    }
}

所以我启动了一个计时器,运行 ConvertPcm32FloatToPcm32Fixed 然后 ConvertPcm32FixedToPcm32Float 直接转换回来,结束计时器。函数的 SSE2 版本总共执行 15-16 微秒,但 AVX 版本需要 22-23 微秒。有点困惑,我进一步挖掘,我发现了如何加速 AVX 版本,以便它们比 SSE2 版本更快,但这是作弊。我只是在启动计时器之前运行 ConvertPcm32FloatToPcm32Fixed,然后启动计时器,然后再次运行 ConvertPcm32FloatToPcm32Fixed,然后运行 ​​ConvertPcm32FixedToPcm32Float,停止计时器。好像 SSE 对 AVX 有很大的惩罚,如果我首先通过试运行“启动”AVX 版本,AVX 执行时间会下降到 12 微秒,而对 SSE 等价物做同样的事情时,只会将时间减少 1 微秒至 14 微秒,使 AVX 成为这里的边际赢家,但前提是我作弊。我认为也许 AVX 在缓存中的表现不如 SSE,但使用 _mm_prefetch 也无济于事。

我在这里错过了什么吗?

4

2 回答 2

5

我没有测试你的代码,但由于你的测试看起来很短,也许你看到了Agner Fog 在他的微架构手册第 101 页讨论的浮点预热效果(这适用于 Sandy Bridge 架构)。我引用:

当处理器有一段时间没有看到任何浮点指令时,处理器处于冷状态。256 位向量加法和乘法的延迟最初比理想数字长两个时钟,然后再长一个时钟,在数百个浮点指令之后,处理器进入暖状态,延迟分别为 3 和 5 个时钟。吞吐量是冷状态下 256 位向量操作的理想值的一半。128 位向量运算受此预热效应的影响较小。128位向量加法和乘法的延迟最多比理想值长一个时钟周期,在冷态下吞吐量不降低。

于 2013-07-17T22:12:50.720 回答
2

我的印象是,除非编译器使用 VEX 指令格式对 SSE 指令进行编码,正如 Paul R 所说 - vmulps 而不是 mulps,否则命中率很高。

在优化小段时,我倾向于使用英特尔的这个好工具和一些很好的基准测试

https://software.intel.com/en-us/articles/intel-architecture-code-analyzer

IACA 生成的报告包括以下符号:

“@ - SSE 指令遵循 AVX256 指令,预计会造成数十个周期的惩罚”

于 2014-09-22T10:17:38.980 回答