Swi*_*ank 4 c++ performance avx dot-product avx512
任务是将数组 A 中的每个浮点数与数组 B 中的相应元素相乘的乘积求和。数组可能有数万个元素,并且必须运行 100,000 倍秒才能处理实时数据流,因此性能是关键。
我使用常规数学对其进行了编码,并再次使用 AVX512 对其进行了编码。它大约快了 10.6 倍,这令人惊讶,因为我预计每条指令执行 16 倍的操作,所以速度会快 16 倍左右。此外,虽然循环有各种开销(例如,循环变量、增量、如果继续循环则分支等),但与原始版本相比,它只执行了 1/16。
我正在 Visual Studio 2022 Community 中以发布模式进行编译,并在 i7-11700F 上运行。
这是代码行。我基本上一次遍历两个数组 16 个元素,将各个元素相乘,并保留 16 个运行和。在计算的最后,我_mm512_reduce_add_ps()对这 16 个和进行求和。
vector<__m512> a512In;
vector<__m512> a512IRCurr;
__m512 fOut = _mm512_set1_ps( 0.0 );
for ( iSample = 0; iSample < iIterations; iSample++ )
fOut = _mm512_add_ps( fOut, _mm512_mul_ps( a512In[ iPos++ ],
a512IRCurr[ iSample ] ) );
Run Code Online (Sandbox Code Playgroud)
我发现vmobups并没有假设目标是一致的,并且想知道这是否是问题所在。不过,我还发现,许多代未对齐版本的速度与对齐版本相同,但令人不安的是延迟可能仍然不同:https ://community.intel.com/t5/Intel-ISA-Extensions/what -are-the-performance-implications-of-using-vmovups-and/mp/1143448 虽然我对 6502 品种的机器语言很满意,但我不了解现代英特尔。
我还想知道这是否_mm512_add_ps是正确的a = a * b构造指令,或者是否有更快的a *= b类型指令。
for ( iSample = 0; iSample < iIterations; iSample++ )
00007FF6677B2958 movsxd r8,edi
00007FF6677B295B test edi,edi
00007FF6677B295D jle Circle2AVR512::ProcessInput+0AEh (07FF6677B298Eh)
fOut = _mm512_add_ps( fOut, _mm512_mul_ps( a512In[ iPos++ ],
00007FF6677B295F movsxd r9,eax
00007FF6677B2962 mov rdx,r11
00007FF6677B2965 shl r9,6
00007FF6677B2969 mov ecx,edi
00007FF6677B296B sub r9,r11
00007FF6677B296E add r9,qword ptr [r10]
00007FF6677B2971 vmovups zmm0,zmmword ptr [r9+rdx]
00007FF6677B2978 vmulps zmm1,zmm0,zmmword ptr [rdx]
00007FF6677B297E lea rdx,[rdx+40h]
00007FF6677B2982 vaddps zmm2,zmm2,zmm1
00007FF6677B2988 sub r8,1
00007FF6677B298C jne Circle2AVR512::ProcessInput+91h (07FF6677B2971h)
a512IRCurr[ iSample ] ) );
Run Code Online (Sandbox Code Playgroud)
TLDR:是内存速度。我的 L1 缓存支持 16-18 倍加速,L2 大约 10-12 倍,L3 大约 4.3 倍,每一个都与简单的每指令单数据 C++ 实现进行比较。 一旦数据不再适合 L1,我的 CPU 就无法充分利用 AVX512 指令的并行性。这就是我狭隘问题的狭隘答案。
更深入的答案:我很不好意思地把回答归功于自己,所以我从帖子的评论中得到了很多想法,但这是我的发现。
该程序逐个样本处理输入数据,这些数据存储在循环队列中,并且对于每个样本,将输入的最后 N 个样本乘以第二个数组中的相应值(称为脉冲响应)。需要明确的是,我将最新的输入样本与脉冲响应数组中的单元格 0 相乘,将第二新的输入样本与单元格 1 相乘,依此类推。这次我在脉冲响应中乘以单元格 0 的最新输入样本,当我再次调用下一个输入样本时,将乘以单元格 1,依此类推。所有这些乘积都会被求和,该和就是函数的输出。
我对这个矩阵乘法做了十几个变体,并使用我称为“Circle2”的基线(一种循环队列,它不检查每个操作的环绕,而是由两个循环组成,其起点和迭代计数在进入循环之前计算)。Circle2AVX512 基本上保持相同的数据结构,但以 16 的倍数步进,并将float*'s 转换为__m512*. 对输入队列的访问碰巧仍然是对齐的,但对脉冲响应的访问大多是不对齐的。Circle2AVX512Aligned 保留 16 个脉冲响应副本,每个可能的内存对齐位置都有一个副本。再次访问始终在循环队列上对齐,但现在也通过选择具有所需对齐方式的脉冲响应版本,在脉冲响应上对齐。
我的 CPU i7-11700F 每个核心具有 32kb L1D(D=数据)缓存和 256kb L2 缓存。然后,16M的L3缓存服务于整个计算机。(我经常上网、YouTube 和文字处理,所以并不是所有这些都可以用于测试。)
看看 Circle2AVX512,只要数据(输入缓冲区加脉冲响应)足够大以克服固定开销但足够小,我的单线程应用程序就能比简单编程但优化的 C++ 实现获得 16-18 倍的加速总共适合 L1--32k,或者换句话说,两个结构各 16k:每个 4000 个样本,采用 4 字节浮点数。
然后,性能下降到比简单的单数据数学提高 10-12 倍的较低水平,直到两个结构增长到超过 256k,此时 L2 不再足够。
此时,它比原始代码保持稳定的 8.4 倍到 9.0 倍加速,至少高达 6.4M(两个 3.2M 数据结构)。
未来的研究方向是看看 L3 缓存完成后它的表现如何。
请注意,内存访问模式是 MRU 缓存的典型最坏情况:线性访问,在您最终要返回数据之前从缓存中嘲笑地删除数据。然而,作为进一步工作的方向,该软件还可以利用 MRU 缓存的经典最佳情况:引用的极端局部性。如果软件在一组中提供多个输入样本,则可以通过在继续之前利用输入队列和脉冲响应的相同范围来高度优化,从而实现极高的参考局部性。计算第一个样本仍然需要从缓存中重新加载所有数据,但下一组输入样本可以搭便车离开已加载的缓存。
坦率地说,我不明白为什么 Circle2AVX512Aligned 算法的性能与我们在这里看到的一样好。它不是一个脉冲响应副本,而是 16 个完全独立的副本。因此,它应该以每个数据结构大约 3.75k 的速度耗尽 L1 缓存(同样,输入队列的字节大小与脉冲响应相同;只有 16 个脉冲响应,而不是一个)。事实上,这就是我们看到它从显着优于非对齐版本转变为表现不佳的地方。但我仍然很困惑,为什么在这个现代 CPU 上,消息来源告诉我应该能够以同样快的速度访问未对齐的内存,为什么对齐的版本要快得多。而且我也很困惑为什么对齐的版本不会比它慢很多。我唯一的猜测是输入队列仍然固定在缓存中,所以至少我们有这样的能力。
同样,当各个结构均为 256k/17=9.17k 时,Circle2AVX512Aligned 不再适合 256k/17 的 L2 缓存。好吧,此时确实出现了差距,但没有我预期的那么大。
最后,当各个结构分别为 16M/17=963k 左右时,Circle2AVX512Aligned 不再适合 16M/17 的 L3 缓存。而这一次,退化更加明显。
| 归档时间: |
|
| 查看次数: |
904 次 |
| 最近记录: |