我试图通过SSE和AVX提高复制操作的性能:
#include <immintrin.h>
const int sz = 1024;
float *mas = (float *)_mm_malloc(sz*sizeof(float), 16);
float *tar = (float *)_mm_malloc(sz*sizeof(float), 16);
float a=0;
std::generate(mas, mas+sz, [&](){return ++a;});
const int nn = 1000;//Number of iteration in tester loops
std::chrono::time_point<std::chrono::system_clock> start1, end1, start2, end2, start3, end3;
//std::copy testing
start1 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
std::copy(mas, mas+sz, tar);
end1 = std::chrono::system_clock::now();
float elapsed1 = std::chrono::duration_cast<std::chrono::microseconds>(end1-start1).count();
//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=4, _tar+=4)
{
__m128 buffer = _mm_load_ps(_mas);
_mm_store_ps(_tar, buffer);
}
}
end2 = std::chrono::system_clock::now();
float elapsed2 = std::chrono::duration_cast<std::chrono::microseconds>(end2-start2).count();
//AVX-copy testing
start3 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m256 buffer = _mm256_load_ps(_mas);
_mm256_store_ps(_tar, buffer);
}
}
end3 = std::chrono::system_clock::now();
float elapsed3 = std::chrono::duration_cast<std::chrono::microseconds>(end3-start3).count();
std::cout<<"serial - "<<elapsed1<<", SSE - "<<elapsed2<<", AVX - "<<elapsed3<<"\nSSE gain: "<<elapsed1/elapsed2<<"\nAVX gain: "<<elapsed1/elapsed3;
_mm_free(mas);
_mm_free(tar);
有效。但是,虽然测试器循环中的迭代次数 - nn - 增加,但是simd-copy的性能提升会降低:
nn = 10:SSE增益= 3,AVX增益= 6;
nn = 100:SSE增益= 0.75,AVX增益= 1.5;
nn = 1000:SSE增益= 0.55,AVX增益= 1.1;
有人可以解释一下提到性能降低效果的原因是什么,是否可以手动矢量化复制操作?
答案 0 :(得分:23)
问题在于,您的测试在硬件中迁移一些使基准测试变得困难的因素方面做得很差。为了测试这个,我做了我自己的测试用例。像这样:
for blah blah:
sleep(500ms)
std::copy
sse
axv
输出:
SSE: 1.11753x faster than std::copy
AVX: 1.81342x faster than std::copy
所以在这种情况下,AVX比std::copy
更快。当我将测试用例更改为..
for blah blah:
sleep(500ms)
sse
axv
std::copy
请注意,除了测试的顺序外,绝对没有任何改变。
SSE: 0.797673x faster than std::copy
AVX: 0.809399x faster than std::copy
哇!怎么可能? CPU需要一段时间才能提升到全速,因此稍后运行的测试具有优势。这个问题现在有3个答案,包括一个“接受”的答案。但只有赞成票数量最少的人才能走上正轨。
这是基准测试很难的原因之一,除非他们已经包含了他们设置的详细信息,否则你永远不要相信任何人的微基准测试。不只是代码可能出错。省电功能和奇怪的驱动程序可能会完全破坏您的基准。有一次,我通过切换不到1%的笔记本电脑提供的BIOS中的开关,测量了7倍的性能差异。
答案 1 :(得分:6)
这是一个非常有趣的问题,但是我认为到目前为止没有答案是正确的,因为问题本身是如此误导。
标题应更改为“如何达到理论内存I / O带宽?”
无论使用何种指令集,CPU都比RAM快得多,因此纯块存储器复制是100%I / O限制。这解释了为什么SSE和AVX性能之间几乎没有区别。
对于L1D缓存中的小缓冲区,AVX可以比像Haswell这样的CPU上的SSE复制速度明显快,其中256b加载/存储确实使用256b数据路径到L1D缓存而不是分成两个128b操作。
具有讽刺意味的是,古老的X86指令 rep stosq 在内存复制方面比SSE和AVX表现更好!
The article here解释了如何充分利用内存带宽,并且它还有很多参考资料可供进一步探索。
另请参阅此处的Enhanced REP MOVSB for memcpy,其中@ BeeOnRope的答案讨论了NT存储(以及rep stosb/stosq
完成的非RFO存储)与常规存储的关系,以及单核内存带宽通常是如何受限的最大并发/延迟,而不是内存控制器本身。
答案 2 :(得分:3)
写快速SSE并不像使用SSE操作代替非并行等价物那么简单。在这种情况下,我怀疑你的编译器无法有效地展开加载/存储对,并且你的时间由在下一条指令(商店)中使用一个低吞吐量操作(负载)的输出引起的停顿所支配。
您可以通过手动展开一个档次来测试这个想法:
//SSE-copy testing
start2 = std::chrono::system_clock::now();
for(int i=0; i<nn; ++i)
{
auto _mas = mas;
auto _tar = tar;
for(; _mas!=mas+sz; _mas+=8, _tar+=8)
{
__m128 buffer1 = _mm_load_ps(_mas);
__m128 buffer2 = _mm_load_ps(_mas+4);
_mm_store_ps(_tar, buffer1);
_mm_store_ps(_tar+4, buffer2);
}
}
通常在使用内在函数时我会反汇编输出并确保没有任何疯狂事件发生(你可以尝试这个来验证原始循环是否/如何展开)。对于更复杂的循环,正确使用的工具是Intel Architecture Code Analyzer (IACA)。它是一个静态分析工具,可以告诉你“你有管道档位”等事情。
答案 3 :(得分:3)
我认为这是因为测量不准确的短期操作。
在英特尔CPU上测量性能时
禁用“Turbo Boost”和“SpeedStep”。你可以在系统BIOS上使用它。
将进程/线程优先级更改为高或实时。这将使您的线程保持运行。
将“处理CPU掩码”设置为仅一个核心。具有更高优先级的CPU屏蔽将最小化上下文切换。
使用__rdtsc()内在函数。 Intel Core系列使用__rdtsc()返回CPU内部时钟计数器。你将从3.4Ghz CPU获得3400000000计数/秒。并且__rdtsc()刷新CPU中的所有计划操作,以便更准确地测量计时。
这是我测试SSE / AVX代码的测试台启动代码。
int GetMSB(DWORD_PTR dwordPtr)
{
if(dwordPtr)
{
int result = 1;
#if defined(_WIN64)
if(dwordPtr & 0xFFFFFFFF00000000) { result += 32; dwordPtr &= 0xFFFFFFFF00000000; }
if(dwordPtr & 0xFFFF0000FFFF0000) { result += 16; dwordPtr &= 0xFFFF0000FFFF0000; }
if(dwordPtr & 0xFF00FF00FF00FF00) { result += 8; dwordPtr &= 0xFF00FF00FF00FF00; }
if(dwordPtr & 0xF0F0F0F0F0F0F0F0) { result += 4; dwordPtr &= 0xF0F0F0F0F0F0F0F0; }
if(dwordPtr & 0xCCCCCCCCCCCCCCCC) { result += 2; dwordPtr &= 0xCCCCCCCCCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAAAAAAAAAA) { result += 1; }
#else
if(dwordPtr & 0xFFFF0000) { result += 16; dwordPtr &= 0xFFFF0000; }
if(dwordPtr & 0xFF00FF00) { result += 8; dwordPtr &= 0xFF00FF00; }
if(dwordPtr & 0xF0F0F0F0) { result += 4; dwordPtr &= 0xF0F0F0F0; }
if(dwordPtr & 0xCCCCCCCC) { result += 2; dwordPtr &= 0xCCCCCCCC; }
if(dwordPtr & 0xAAAAAAAA) { result += 1; }
#endif
return result;
}
else
{
return 0;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
// Set Core Affinity
DWORD_PTR processMask, systemMask;
GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask);
SetProcessAffinityMask(GetCurrentProcess(), 1 << (GetMSB(processMask) - 1) );
// Set Process Priority. you can use REALTIME_PRIORITY_CLASS.
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
DWORD64 start, end;
start = __rdtsc();
// your code here.
end = __rdtsc();
printf("%I64d\n", end - start);
return 0;
}
答案 4 :(得分:1)
我认为您的主要问题/瓶颈是_mm_malloc
。
如果您担心C ++中的位置,我强烈建议您使用std::vector
作为主要数据结构。
intrinsics 不完全是一个“库”,它们更像是从编译器提供给你的内置函数,你应该熟悉你的编译器内部/文档在使用此功能之前。
另请注意,AVX
比SSE
更新的事实并不能使AVX
更快,无论您打算使用什么,周期数都是函数可能比“avx vs sse”参数更重要,例如参见this answer。
尝试使用POD int array[]
或std::vector
。