SSE拷贝、AVX拷贝和std::拷贝性能
SSE-copy, AVX-copy and std::copy performance
我试图通过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副本的性能增益降低:
nn=10:SSE增益=3,AVX增益=6;
nn=100:SSE增益=0.75,AVX增益=1.5;
nn=1000:SSE增益=0.55,AVX增益=1.1;
有人能解释一下上述性能下降效应的原因是什么吗?手动向量化复制操作是否可取?
问题是您的测试在迁移硬件中的一些因素方面做得很差,这些因素使基准测试变得困难。为了测试这一点,我制作了自己的测试用例。类似这样的东西:
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个答案,包括一个"已接受"的答案。但只有支持率最低的人走上了正轨。
这就是为什么基准测试很难的原因之一,你永远不应该相信任何人的微观基准测试,除非他们包含了详细的设置信息。不仅仅是代码会出错。节能功能和奇怪的驱动程序会完全打乱你的基准测试。有一次,我通过在bios中切换开关来测量性能的7倍差异,而只有不到1%的笔记本电脑提供这种差异。
这是一个非常有趣的问题,但我认为到目前为止没有一个答案是正确的,因为这个问题本身具有误导性
标题应更改为"如何达到理论内存I/O带宽?"
无论使用什么指令集,CPU都比RAM快得多,以至于纯块内存拷贝是100%有I/O限制的。这就解释了为什么SSE和AVX的性能差别不大。
对于L1D缓存中的小缓冲区,AVX可以比Haswell等CPU上的SSE复制快得多,在Haswell中,256b加载/存储确实使用了到L1D缓存的256b数据路径,而不是拆分为两个128b操作。
具有讽刺意味的是,在内存复制方面,古老的X86指令rep-stosq的性能要比SSE和AVX好得多!
这里的文章解释了如何很好地饱和内存带宽,它也有丰富的参考资料可以进一步探索。
另请参阅SO上的Enhanced REP MOVSB中的memcpy,其中@BeeOnRope的回答讨论了NT存储(以及由rep stosb/stosq
完成的非RFO存储)与常规存储之间的关系,以及单核内存带宽通常是如何受到最大并发/延迟的限制,而不是内存控制器本身的限制。
编写快速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);
}
}
通常,当使用内部函数时,我会分解输出,并确保没有发生任何疯狂的事情(你可以尝试验证原始循环是否/如何展开)。对于更复杂的循环,正确的工具是"英特尔体系结构代码分析器"(IACA)。这是一个静态分析工具,可以告诉你"你有管道停滞"之类的事情。
我认为这是因为测量对于一些短操作来说不准确。
在英特尔CPU 上测量性能时
-
禁用";Turbo Boost";以及";SpeedStep";。您可以在系统BIOS上进行此操作。
-
将进程/线程优先级更改为"高"或"实时"。这将保持线程运行。
-
将进程CPU掩码设置为仅一个核心。具有更高优先级的CPU掩码将最大限度地减少上下文切换。
-
使用CCD_ 3内在函数。Intel Core系列返回带有
__rdtsc()
的CPU内部时钟计数器。您将从3.4Ghz CPU获得340000000计数/秒。__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("%I64dn", end - start);
return 0;
}
我认为您的主要问题/瓶颈是_mm_malloc
。
如果您关心C++中的局部性,我强烈建议使用std::vector
作为您的主要数据结构。
内部函数并不完全是一个"库",它们更像是编译器提供给您的内置函数,在使用这些函数之前,您应该熟悉编译器内部/文档。
还要注意的是,AVX
比SSE
新这一事实并不能使AVX
更快,无论您计划使用什么,函数所用的周期数可能比"avx vs sse"参数更重要,例如,请参阅此答案。
尝试使用POD int array[]
或std::vector
。
- 删除一个线程上有数百万个字符串的大型哈希映射会影响另一个线程的性能
- OpenMP阵列性能较差
- 递归列出所有目录中的C++与Python与Ruby的性能
- 大小相等但成员数量不同的结构之间的性能差异
- 为什么constexpr的性能比正常表达式差
- 在类中使用随机生成器时出现性能问题
- 在main()之外初始化std::vector会导致性能下降(多线程)
- 如何在没有数据拷贝的情况下从指针创建一个Eigen VectorXd对象
- 海湾合作委员会 ARM 性能下降
- GCC 和 Clang 代码性能的巨大差异
- 在容量内调整矢量大小时的性能影响
- 了解算法的性能差异(如果以不同的编程语言实现)
- 未达到的情况会影响开关外壳性能
- QStringList vs list<shared_ptr<QString>> 性能比较C++
- 是否总是可以将使用递归编写的程序重写为不使用递归的程序C++,性能观点是什么?
- 与浅拷贝构造函数和深拷贝构造函数混淆
- 哪种方法更好,性能明智
- C++ 特征库:引用的性能开销<>
- Cuda零拷贝性能
- SSE拷贝、AVX拷贝和std::拷贝性能