我对我的酷睿 i7 920 的 FP 峰值性能有疑问。 我有一个执行大量 MAC 运算(基本上是卷积运算)的应用程序,当使用多线程和 SSE 指令时,我无法将 cpu 的峰值 FP 性能提高约 8 倍。 当试图找出原因时,我最终得到了一个简化的代码片段,在单个线程上运行并且不使用性能同样糟糕的 SSE 指令:
for(i=0; i<49335264; i++)
{
data[i] += other_data[i] * other_data2[i];
}
如果我是正确的(data 和 other_data 数组都是 FP),这段代码需要:
49335264 * 2 = 98670528 FLOPs
它在大约 150 毫秒内执行(我非常确定这个计时是正确的,因为 C 定时器和英特尔 VTune Profiler 给了我相同的结果)
这意味着这段代码片段的性能是:
98670528 / 150.10^-3 / 10^9 = 0.66 GFLOPs/sec
该 cpu 的峰值性能应该为 2*3.2 GFlops/sec(2 个 FP 单元,3.2 GHz 处理器),对吗?
对于如此巨大的差距有什么解释吗?因为我无法解释。
提前非常感谢,我真的需要你的帮助!
我会使用SSE。
编辑:我自己又进行了一些测试,发现你的程序既不受内存带宽的限制(理论限制大约比你的结果高3-4倍),也不受浮点性能(有更高的限制),它是受操作系统延迟分配内存页面的限制。
#include <chrono>
#include <iostream>
#include <x86intrin.h>
using namespace std::chrono;
static const unsigned size = 49335264;
float data[size], other_data[size], other_data2[size];
int main() {
#if 0
for(unsigned i=0; i<size; i++) {
data[i] = i;
other_data[i] = i;
other_data2[i] = i;
}
#endif
system_clock::time_point start = system_clock::now();
for(unsigned i=0; i<size; i++)
data[i] += other_data[i]*other_data2[i];
microseconds timeUsed = system_clock::now() - start;
std::cout << "Used " << timeUsed.count() << " us, "
<< 2*size/(timeUsed.count()/1e6*1e9) << " GFLOPS\n";
}
用
g++ -O3 -march=native -std=c++0x
翻译。该程序给出
Used 212027 us, 0.465368 GFLOPS
作为输出,尽管热循环转换为
400848: vmovaps 0xc234100(%rdx),%ymm0
400850: vmulps 0x601180(%rdx),%ymm0,%ymm0
400858: vaddps 0x17e67080(%rdx),%ymm0,%ymm0
400860: vmovaps %ymm0,0x17e67080(%rdx)
400868: add $0x20,%rdx
40086c: cmp $0xbc32f80,%rdx
400873: jne 400848 <main+0x18>
这意味着它是完全矢量化的,每次迭代使用 8 个浮点,甚至利用 AVX。 在尝试了像
movntdq
这样的流指令(它没有购买任何东西)之后,我决定用一些东西来实际初始化数组 - 否则它们将是零页,只有在写入时才会映射到真实内存。将 #if 0
更改为 #if 1
立即产生
Used 48843 us, 2.02016 GFLOPS
这非常接近系统的内存带宽(4 个浮点,每两次 FLOPS 4 个字节 = 16 GBytes/s,理论限制是 2 个 DDR3 通道,每个通道 10,667 GBytes/s)。
解释很简单:虽然您的处理器可以在(例如)6.4GHz 下运行,但您的内存子系统只能以大约 1/10 的速率输入/输出数据(大多数当前商品 CPU 的广泛经验法则) 。 因此,实现处理器理论最大值的 1/8 的持续失败率实际上是非常好的性能。
由于您似乎正在处理大约 370MB 的数据,这可能大于处理器上的缓存,因此您的计算受 I/O 限制。
正如 High Performance Mark 所解释的,您的测试很可能是内存限制而不是计算限制。
我想补充的一件事是,为了量化这种影响,您可以修改测试,以便它对适合 L1 缓存的数据进行操作:
for(i=0, j=0; i<6166908; i++)
{
data[j] += other_data[j] * other_data2[j]; j++;
data[j] += other_data[j] * other_data2[j]; j++;
data[j] += other_data[j] * other_data2[j]; j++;
data[j] += other_data[j] * other_data2[j]; j++;
data[j] += other_data[j] * other_data2[j]; j++;
data[j] += other_data[j] * other_data2[j]; j++;
data[j] += other_data[j] * other_data2[j]; j++;
data[j] += other_data[j] * other_data2[j]; j++;
if ((j & 1023) == 0) j = 0;
}
此版本代码的性能应该更接近 FLOPS 的理论最大值。当然,它可能无法解决您原来的问题,但希望它可以帮助您理解发生了什么。
我在第一篇文章中查看了代码片段的乘法累加的汇编代码,它看起来像:
movq 0x80(%rbx), %rcx
movq 0x138(%rbx), %rdi
movq 0x120(%rbx), %rdx
movq (%rcx), %rsi
movq 0x8(%rdi), %r8
movq 0x8(%rdx), %r9
movssl 0x1400(%rsi), %xmm0
mulssl 0x90(%r8), %xmm0
addssl 0x9f8(%r9), %xmm0
movssl %xmm0, 0x9f8(%r9)
我根据总周期数估计执行乘法累加需要大约 10 个周期。
问题似乎是编译器无法管道化循环的执行,即使没有循环间依赖关系,我是对的吗?
有人对此有任何其他想法/解决方案吗?
感谢迄今为止的帮助!