[英]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::vector
或std::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注意相同的矢量sin
和atan
调用:
$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
其他注意事项:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.