繁体   English   中英

在简单的基准测试中,AVX比IA32慢3.6倍 <cmath> 运营 - 为什么会这样? (VS2013)

[英]AVX 3.6x slower than IA32 in simple benchmark involving <cmath> operations - why so? (VS2013)

我将在前言中说C ++不是我典型的工作领域,我更常用于C#和Matlab。 我也不假装能够阅读x86汇编代码。 最近看过一些关于“现代c ++”的视频和最新处理器的新说明,我想我会更多地看一下,看看我能学到什么。 我确实有一些现有的C ++ DLL可以从速度改进中受益 - 这些DLL使用来自<cmath>许多触发和电源操作。

所以我在VS2013 Express / Desktop中编写了一个简单的基准程序。 我的机器上的处理器是Intel i7-4800MQ(Haswell)。 程序非常简单,将一些std::vector<double>分配给500万随机条目的大小,然后循环执行组合这些值的一些数学运算。 我测量在循环之前和之后使用std::chrono::high_resolution_clock::now()花费的时间:

[编辑:包括完整的程序代码]

#include "stdafx.h"
#include <chrono>
#include <random>
#include <cmath>
#include <iostream>
#include <string>

int _tmain(int argc, _TCHAR* argv[])
{

    // Set up random number generator
    std::tr1::mt19937 eng;
    std::tr1::normal_distribution<float> dist;

    // Number of calculations to do
    uint32_t n_points = 5000000;

    // Input vectors
    std::vector<double> x1;
    std::vector<double> x2;
    std::vector<double> x3;

    // Output vectors
    std::vector<double> y1;

    // Initialize
    x1.reserve(n_points);
    x2.reserve(n_points);
    x3.reserve(n_points);
    y1.reserve(n_points);

    // Fill inputs
    for (size_t i = 0; i < n_points; i++)
    {
        x1.push_back(dist(eng));
        x2.push_back(dist(eng));
        x3.push_back(dist(eng));
    }

    // Start timer
    auto start_time = std::chrono::high_resolution_clock::now();

    // Do math loop
    for (size_t i = 0; i < n_points; i++)
    {
        double result_value; 

        result_value = std::sin(x1[i]) * x2[i] * std::atan(x3[i]);

        y1.push_back(result_value);
    }

    auto end_time = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
    std::cout << "Duration: " << duration.count() << " ms";

    return 0;
}

我使用标准选项(例如/ O2)将VS置于Release配置中。 我使用/ arch执行一次构建:IA32并运行几次,另一次运行/ arch:AVX并运行几次。 一致地说,放置AVX选项比IA32选择慢约3.6倍。 在这个具体的例子中,与之相比,调整为773毫秒。

作为一个完整性检查,我确实尝试了一些其他非常基本的操作..结合了mults并添加..将一些数字带到第8个电源......并且在两个AVX之间至少同样快,如果不是更快一点。 那么为什么上面的代码会受到太多影响呢? 或者我可以在哪里找到?

编辑2:根据Reddit上某人的建议,我将代码更改为更可矢量化的内容......这使得SSE2和AVX运行得更快,但AVX仍然比SSE2慢得多:

#include "stdafx.h"
#include <chrono>
#include <random>
#include <cmath>
#include <iostream>
#include <string>

int _tmain(int argc, _TCHAR* argv[])
{

    // Set up random number generator
    std::tr1::mt19937 eng;
    std::tr1::normal_distribution<double> dist;

    // Number of calculations to do
    uint32_t n_points = 5000000;

    // Input vectors
    std::vector<double> x1;
    std::vector<double> x2;
    std::vector<double> x3;

    // Output vectors
    std::vector<double> y1;

    // Initialize
    x1.reserve(n_points);
    x2.reserve(n_points);
    x3.reserve(n_points);
    y1.reserve(n_points);

    // Fill inputs
    for (size_t i = 0; i < n_points; i++)
    {
        x1.push_back(dist(eng));
        x2.push_back(dist(eng));
        x3.push_back(dist(eng));
        y1.push_back(0.0);
    }

    // Start timer
    auto start_time = std::chrono::high_resolution_clock::now();

    // Do math loop
    for (size_t i = 0; i < n_points; i++)
    {
        y1[i] = std::sin(x1[i]) * x2[i] * std::atan(x3[i]);
    }

    auto end_time = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
    std::cout << "Duration: " << duration.count() << " ms";

    return 0;
}

IA32:209 ms SSE:205 ms SSE2:75 ms AVX:371 ms

至于Visual Studio的特定版本,这是2013 Express for Desktop Update 1(版本12.0.30110.00 Update 1)

当CPU在使用AVX和SSE指令之间切换时,它需要保存/恢复ymm寄存器的上半部分,并且可能导致相当大的损失

通常使用/arch:AVX编译/arch:AVX将为您自己的代码修复此问题,因为它将尽可能使用AVX128指令而不是SSE指令。 但是在这种情况下,可能是您的标准库的数学函数没有使用AVX指令实现,在这种情况下,您将获得每个函数调用的转换惩罚。 你必须发布一个反汇编的版本才能确定。

您经常会看到VZEROUPPER在转换之前被调用,表示CPU不需要保存寄存器的上半部分,但编译器不够聪明,无法知道它调用的函数是否也需要它。

所以基于@LưuVĩnhPhúc我调查了一下,你可以很好地实现矢量化但不使用std::vectorstd::valarray ,当我使用std::unique_ptr时我也不得不对指针进行别名会阻止矢量化。

#include <chrono>
#include <random>
#include <math.h>
#include <iostream>
#include <string>
#include <valarray>
#include <functional>
#include <memory>

#pragma intrinsic(sin, atan)
int wmain(int argc, wchar_t* argv[])
{

    // Set up random number generator
    std::random_device rd;
    std::mt19937 eng(rd());
    std::normal_distribution<double> dist;

    // Number of calculations to do
    const uint32_t n_points = 5000000;

    // Input vectors
    std::unique_ptr<double[]> x1 = std::make_unique<double[]>(n_points);
    std::unique_ptr<double[]> x2 = std::make_unique<double[]>(n_points);
    std::unique_ptr<double[]> x3 = std::make_unique<double[]>(n_points);

    // Output vectors
    std::unique_ptr<double[]> y1 = std::make_unique<double[]>(n_points);
    auto random = std::bind(dist, eng);
    // Fill inputs
    for (size_t i = 0; i < n_points; i++)
    {
        x1[i] = random();
        x2[i] = random();
        x3[i] = random();
        y1[i] = 0.0;
    }

    // Start timer
    auto start_time = std::chrono::high_resolution_clock::now();

    // Do math loop
    double * x_1 = x1.get(), *x_2 = x2.get(), *x_3 = x3.get(), *y_1 = y1.get();
    for (size_t i = 0; i < n_points; ++i)
    {
        y_1[i] = sin(x_1[i]) * x_2[i] * atan(x_3[i]);
    }

    auto end_time = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
    std::cout << "Duration: " << duration.count() << " ms";
    std::cin.ignore();
    return 0;
}

在用/arch:avx编译的机器上,这需要103ms, /arch:IA32 :252ms,没有设置:98ms

看看生成的程序集,似乎矢量函数是使用SSE实现的,因此使用它们周围的AVX指令会导致阻抗并减慢速度。 希望MS将来能够实现AVX版本。

相关的asm缺少vzeroupper

$LL3@wmain:
    vmovupd xmm0, XMMWORD PTR [esi]
    call    ___vdecl_sin2
    mov eax, DWORD PTR tv1250[esp+10212]
    vmulpd  xmm0, xmm0, XMMWORD PTR [eax+esi]
    mov eax, DWORD PTR tv1249[esp+10212]
    vmovaps XMMWORD PTR tv1240[esp+10212], xmm0
    vmovupd xmm0, XMMWORD PTR [eax+esi]
    call    ___vdecl_atan2
    dec DWORD PTR tv1260[esp+10212]
    lea esi, DWORD PTR [esi+16]
    vmulpd  xmm0, xmm0, XMMWORD PTR tv1240[esp+10212]
    vmovupd XMMWORD PTR [edi+esi-16], xmm0
    jne SHORT $LL3@wmain

与SSE2 asm注意相同的矢量sinatan调用:

$LL3@wmain:
    movupd  xmm0, XMMWORD PTR [esi]
    call    ___vdecl_sin2
    mov eax, DWORD PTR tv1250[esp+10164]
    movupd  xmm1, XMMWORD PTR [eax+esi]
    mov eax, DWORD PTR tv1249[esp+10164]
    mulpd   xmm0, xmm1
    movaps  XMMWORD PTR tv1241[esp+10164], xmm0
    movupd  xmm0, XMMWORD PTR [eax+esi]
    call    ___vdecl_atan2
    dec DWORD PTR tv1260[esp+10164]
    lea esi, DWORD PTR [esi+16]
    movaps  xmm1, XMMWORD PTR tv1241[esp+10164]
    mulpd   xmm1, xmm0
    movupd  XMMWORD PTR [edi+esi-16], xmm1
    jne SHORT $LL3@wmain

其他注意事项:

  • VS仅使用AVX寄存器的底部128位,尽管宽度为256位
  • AVX的矢量函数没有重载
  • AVX2尚不支持

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM