【问题标题】:float point multiplication: LOSING speed with AVX against SSE?浮点乘法:AVX 对 SSE 的速度下降?
【发布时间】:2015-05-15 20:57:42
【问题描述】:

我有做同样事情的代码,但 AVX 版本比 SSE 版本慢得多。谁能解释一下?

我已经做过的是,我尝试使用 VerySleepy 分析代码,但这无法给我任何有用的结果,它只是确认它更慢......

我已经查看了 SSE/AVX 指南中的命令,在我的 CPU (Haswell) 上,它们需要相同的延迟/吞吐量,只是水平添加需要额外的 AVX 命令...

** 延迟和吞吐量 **

_mm_mul_ps            -> L 5, T 0.5
_mm256_mul_ps         -> L 5, T 0.5
_mm_hadd_ps           -> L 5, T 2
_mm256_hadd_ps        -> L 5, T ?
_mm256_extractf128_ps -> L 1, T 1

代码的作用总结: 最终1 = SUM( m_Array1 * m_Array1 * m_Array3 * m_Array3 )

Final2 = SUM( m_Array2 * m_Array2 * m_Array3 * m_Array3 )

Final3 = SUM( m_Array1 * m_Array2 * m_Array3 * m_Array3 )

初始化

float Final1 = 0.0f;
float Final2 = 0.0f;
float Final3 = 0.0f;

float* m_Array1 = (float*)_mm_malloc( 32 * sizeof( float ), 32 );
float* m_Array2 = (float*)_mm_malloc( 32 * sizeof( float ), 32 );
float* m_Array3 = (float*)_mm_malloc( 32 * sizeof( float ), 32 );

上海证券交易所:

for ( int k = 0; k < 32; k += 4 )
{

    __m128 g1 = _mm_load_ps( m_Array1 + k );
    __m128 g2 = _mm_load_ps( m_Array2 + k );
    __m128 g3 = _mm_load_ps( m_Array3 + k );

    __m128 g1g3 = _mm_mul_ps( g1, g3 );
    __m128 g2g3 = _mm_mul_ps( g2, g3 );

    __m128 a1 = _mm_mul_ps( g1g3, g1g3 );
    __m128 a2 = _mm_mul_ps( g2g3, g2g3 );
    __m128 a3 = _mm_mul_ps( g1g3, g2g3 );

    // horizontal add
    {
        a1 = _mm_hadd_ps( a1, a1 );
        a1 = _mm_hadd_ps( a1, a1 );
        Final1 += _mm_cvtss_f32( a1 );

        a2 = _mm_hadd_ps( a2, a2 );
        a2 = _mm_hadd_ps( a2, a2 );
        Final2 += _mm_cvtss_f32( a2 );

        a3 = _mm_hadd_ps( a3, a3 );
        a3 = _mm_hadd_ps( a3, a3 );
        Final3 += _mm_cvtss_f32( a3 );

    }

}

AVX:

for ( int k = 0; k < 32; k += 8 )
{
    __m256 g1 = _mm256_load_ps( m_Array1 + k );
    __m256 g2 = _mm256_load_ps( m_Array2 + k );
    __m256 g3 = _mm256_load_ps( m_Array3 + k );

    __m256 g1g3 = _mm256_mul_ps( g1, g3 );
    __m256 g2g3 = _mm256_mul_ps( g2, g3 );

    __m256 a1 = _mm256_mul_ps( g1g3, g1g3 );
    __m256 a2 = _mm256_mul_ps( g2g3, g2g3 );
    __m256 a3 = _mm256_mul_ps( g1g3, g2g3 );

    // horizontal add1
    {
        __m256 t1 = _mm256_hadd_ps( a1, a1 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        Final1 += _mm_cvtss_f32( t4 );
    }
    // horizontal add2
    {
        __m256 t1 = _mm256_hadd_ps( a2, a2 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        Final2 += _mm_cvtss_f32( t4 );
    }
    // horizontal add3
    {
        __m256 t1 = _mm256_hadd_ps( a3, a3 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        Final3 += _mm_cvtss_f32( t4 );
    }

}

【问题讨论】:

  • 您需要一个 更大的循环来可靠地对这两个代码片段进行基准测试。此外,在这两种情况下,水平相加和最终标量值提取都应该在循环之外。
  • 这只是一个小例子。将 32 替换为“some-integer-times-8”,问题依旧
  • @SH:当然,但是您的代码在这两种情况下都非常低效 - 如果您将水平添加和标量提取移出循环并在循环中使用正常(垂直)添加,那么您在这两种情况下都会获得更高效的代码,并且由于指令组合的不同,相对性能也可能会发生变化。
  • 你为什么还要这样做呢?您可以轻松地几乎完全垂直地完成它,只在循环后水平求和一次
  • @SH:因为你只是对所有的产品求和,你可以在循环中做一个垂直加法,给出四个部分和,然后在循环之后做一个水平加法来组合这四个部分总和。这将更加高效,并避免上述高延迟指令。

标签: c++ performance sse avx


【解决方案1】:

我把你的代码放到一个测试工具中,编译它clang -O3 并计时。我还实现了这两个例程的更快版本,将水平添加移出循环:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <sys/time.h>   // gettimeofday
#include <immintrin.h>

static void sse(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    for (int k = 0; k < n; k += 4)
    {
        __m128 g1 = _mm_load_ps( m_Array1 + k );
        __m128 g2 = _mm_load_ps( m_Array2 + k );
        __m128 g3 = _mm_load_ps( m_Array3 + k );

        __m128 g1g3 = _mm_mul_ps( g1, g3 );
        __m128 g2g3 = _mm_mul_ps( g2, g3 );

        __m128 a1 = _mm_mul_ps( g1g3, g1g3 );
        __m128 a2 = _mm_mul_ps( g2g3, g2g3 );
        __m128 a3 = _mm_mul_ps( g1g3, g2g3 );

        // horizontal add
        {
            a1 = _mm_hadd_ps( a1, a1 );
            a1 = _mm_hadd_ps( a1, a1 );
            *Final1 += _mm_cvtss_f32( a1 );

            a2 = _mm_hadd_ps( a2, a2 );
            a2 = _mm_hadd_ps( a2, a2 );
            *Final2 += _mm_cvtss_f32( a2 );

            a3 = _mm_hadd_ps( a3, a3 );
            a3 = _mm_hadd_ps( a3, a3 );
            *Final3 += _mm_cvtss_f32( a3 );
        }
    }
}

static void sse_fast(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    __m128 a1 = _mm_setzero_ps();
    __m128 a2 = _mm_setzero_ps();
    __m128 a3 = _mm_setzero_ps();
    for (int k = 0; k < n; k += 4)
    {
        __m128 g1 = _mm_load_ps( m_Array1 + k );
        __m128 g2 = _mm_load_ps( m_Array2 + k );
        __m128 g3 = _mm_load_ps( m_Array3 + k );

        __m128 g1g3 = _mm_mul_ps( g1, g3 );
        __m128 g2g3 = _mm_mul_ps( g2, g3 );

        a1 = _mm_add_ps(a1, _mm_mul_ps( g1g3, g1g3 ));
        a2 = _mm_add_ps(a2, _mm_mul_ps( g2g3, g2g3 ));
        a3 = _mm_add_ps(a3, _mm_mul_ps( g1g3, g2g3 ));
    }
    // horizontal add
    a1 = _mm_hadd_ps( a1, a1 );
    a1 = _mm_hadd_ps( a1, a1 );
    *Final1 += _mm_cvtss_f32( a1 );

    a2 = _mm_hadd_ps( a2, a2 );
    a2 = _mm_hadd_ps( a2, a2 );
    *Final2 += _mm_cvtss_f32( a2 );

    a3 = _mm_hadd_ps( a3, a3 );
    a3 = _mm_hadd_ps( a3, a3 );
    *Final3 += _mm_cvtss_f32( a3 );
}

static void avx(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    for (int k = 0; k < n; k += 8 )
    {
        __m256 g1 = _mm256_load_ps( m_Array1 + k );
        __m256 g2 = _mm256_load_ps( m_Array2 + k );
        __m256 g3 = _mm256_load_ps( m_Array3 + k );

        __m256 g1g3 = _mm256_mul_ps( g1, g3 );
        __m256 g2g3 = _mm256_mul_ps( g2, g3 );

        __m256 a1 = _mm256_mul_ps( g1g3, g1g3 );
        __m256 a2 = _mm256_mul_ps( g2g3, g2g3 );
        __m256 a3 = _mm256_mul_ps( g1g3, g2g3 );

        // horizontal add1
        {
            __m256 t1 = _mm256_hadd_ps( a1, a1 );
            __m256 t2 = _mm256_hadd_ps( t1, t1 );
            __m128 t3 = _mm256_extractf128_ps( t2, 1 );
            __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
            *Final1 += _mm_cvtss_f32( t4 );
        }
        // horizontal add2
        {
            __m256 t1 = _mm256_hadd_ps( a2, a2 );
            __m256 t2 = _mm256_hadd_ps( t1, t1 );
            __m128 t3 = _mm256_extractf128_ps( t2, 1 );
            __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
            *Final2 += _mm_cvtss_f32( t4 );
        }
        // horizontal add3
        {
            __m256 t1 = _mm256_hadd_ps( a3, a3 );
            __m256 t2 = _mm256_hadd_ps( t1, t1 );
            __m128 t3 = _mm256_extractf128_ps( t2, 1 );
            __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
            *Final3 += _mm_cvtss_f32( t4 );
        }
    }
}

static void avx_fast(const float *m_Array1, const float *m_Array2, const float *m_Array3, size_t n, float *Final1, float *Final2, float *Final3)
{
    *Final1 = *Final2 = *Final3 = 0.0f;
    __m256 a1 = _mm256_setzero_ps();
    __m256 a2 = _mm256_setzero_ps();
    __m256 a3 = _mm256_setzero_ps();
    for (int k = 0; k < n; k += 8 )
    {
        __m256 g1 = _mm256_load_ps( m_Array1 + k );
        __m256 g2 = _mm256_load_ps( m_Array2 + k );
        __m256 g3 = _mm256_load_ps( m_Array3 + k );

        __m256 g1g3 = _mm256_mul_ps( g1, g3 );
        __m256 g2g3 = _mm256_mul_ps( g2, g3 );

        a1 = _mm256_add_ps(a1, _mm256_mul_ps( g1g3, g1g3 ));
        a2 = _mm256_add_ps(a2, _mm256_mul_ps( g2g3, g2g3 ));
        a3 = _mm256_add_ps(a3, _mm256_mul_ps( g1g3, g2g3 ));
    }

    // horizontal add1

    {
        __m256 t1 = _mm256_hadd_ps( a1, a1 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        *Final1 += _mm_cvtss_f32( t4 );
    }

    // horizontal add2

    {
        __m256 t1 = _mm256_hadd_ps( a2, a2 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        *Final2 += _mm_cvtss_f32( t4 );
    }

    // horizontal add3

    {
        __m256 t1 = _mm256_hadd_ps( a3, a3 );
        __m256 t2 = _mm256_hadd_ps( t1, t1 );
        __m128 t3 = _mm256_extractf128_ps( t2, 1 );
        __m128 t4 = _mm_add_ss( _mm256_castps256_ps128( t2 ), t3 );
        *Final3 += _mm_cvtss_f32( t4 );
    }

}

int main(int argc, char *argv[])
{
    size_t n = 4096;

    if (argc > 1) n = atoi(argv[1]);

    float *in_1 = valloc(n * sizeof(in_1[0]));
    float *in_2 = valloc(n * sizeof(in_2[0]));
    float *in_3 = valloc(n * sizeof(in_3[0]));
    float out_1, out_2, out_3;

    struct timeval t0, t1;
    double t_ms;

    for (int i = 0; i < n; ++i)
    {
        in_1[i] = (float)rand() / (float)(RAND_MAX / 2);
        in_2[i] = (float)rand() / (float)(RAND_MAX / 2);
        in_3[i] = (float)rand() / (float)(RAND_MAX / 2);
    }

    sse(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("sse     : %g, %g, %g\n", out_1, out_2, out_3);
    sse_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("sse_fast: %g, %g, %g\n", out_1, out_2, out_3);
    avx(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("avx     : %g, %g, %g\n", out_1, out_2, out_3);
    avx_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    printf("avx_fast: %g, %g, %g\n", out_1, out_2, out_3);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) sse(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("sse     : %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) sse_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("sse_fast: %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) avx(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("avx     : %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    gettimeofday(&t0, NULL);
    for (int k = 0; k < 100; ++k) avx_fast(in_1, in_2, in_3, n, &out_1, &out_2, &out_3);
    gettimeofday(&t1, NULL);
    t_ms = ((double)(t1.tv_sec - t0.tv_sec) + (double)(t1.tv_usec - t0.tv_usec) * 1.0e-6) * 1.0e3;
    printf("avx_fast: %g, %g, %g, %g ms\n", out_1, out_2, out_3, t_ms);

    return 0;
}

在我的 2.6 GHz Haswell (MacBook Pro) 上的结果是:

sse     : 0.439 ms
sse_fast: 0.153 ms
avx     : 0.309 ms
avx_fast: 0.085 ms

因此,对于原始实现和优化实现而言,AVX 版本确实似乎比 SSE 版本更快。优化后的实现速度明显快于原始版本,但幅度更大。

我只能猜测您的编译器没有为 AVX 生成非常好的代码(或者您可能忘记启用编译器优化?),或者您的基准测试方法可能存在问题。

【讨论】:

  • 这真的很奇怪,我会检查并发布我的结果 - 如果有的话
  • 通过您的代码,我明白您所说的“将 hadd 移出循环”是什么意思。我以为你想以某种方式只先乘以(没有最后一个加法),然后神奇地总结它们。谢谢!
  • 是的,根据经验,在 SIMD 代码中通常应避免水平操作,当它们无法避免时,最好将它们置于任何性能关键循环之外。
  • 我正在使用 VS2012、“-O3”和“/arch:AVX”进行编译。这可能是个问题吗?
  • 我原以为 /O2 或 /Ox 都可以使用 Visual Studio,但我在 Windows 上并没有做太多工作,我不确定 Visual Studio 的性能如何涉及到 AVX 代码生成。 gcc、clang 和 Intel 的 ICC 都可以生成非常好的 AVX 代码,因此您可能需要考虑尝试不同的编译器。您可能还想查看您的基准测试方法,因为粗心的人有很多陷阱。如果你可以让我上面的测试工具用 Visual Studio 编译,那么看看你得到的时间数字会很有趣。
猜你喜欢
  • 2022-11-02
  • 2011-09-30
  • 2021-03-25
  • 2011-07-05
  • 1970-01-01
  • 2015-08-15
  • 1970-01-01
  • 1970-01-01
  • 2016-09-02
相关资源
最近更新 更多