我见过这个博客:
http://igoro.com/archive/gallery-of-processor-cache-effects/
第7部分中的“古怪”引起了我的兴趣。
我的第一个想法是“那就是C#很奇怪”。
我没有写下面的C ++代码。
volatile int* p = (volatile int*)_aligned_malloc( sizeof( int ) * 8, 64 );
memset( (void*)p, 0, sizeof( int ) * 8 );
double dStart = t.GetTime();
for (int i = 0; i < 200000000; i++)
{
//p[0]++;p[1]++;p[2]++;p[3]++; // Option 1
//p[0]++;p[2]++;p[4]++;p[6]++; // Option 2
p[0]++;p[2]++; // Option 3
}
double dTime = t.GetTime() - dStart;
我在2.4 Ghz Core 2 Quad上的时间如下:
Option 1 = ~8 cycles per loop.
Option 2 = ~4 cycles per loop.
Option 3 = ~6 cycles per loop.
现在这令人困惑。差异背后的原因归结为我的芯片上的缓存写入延迟(3个周期)以及缓存具有128位写入端口的假设(这对我来说是纯粹的猜测工作)。
在此基础上选项1:它将递增p [0](1个周期)然后递增p [2](1个周期)然后它必须等待1个周期(对于缓存)然后p [1](1个周期)然后等待1个循环(用于缓存)然后等待[3](1个循环)。最后增加和跳转2个周期(虽然它通常实现为递减和跳转)。这总共给出了8个周期。
在选项2中:它可以在一个周期内递增p [0]和p [4],然后在另一个周期中递增p [2]和p [6]。然后2个循环进行减法和跳跃。缓存不需要等待。共4个周期。
在选项3中:它可以递增p [0]然后必须等待2个周期然后递增p [2]然后减去并跳转。问题是如果你将case 3设置为递增p [0]和p [4],它仍然需要6个周期(这会使我的128位读/写端口脱离水中)。
所以...谁能告诉我这到底是怎么回事?为什么案例3需要更长时间?我也很想知道上面我的想法是什么,因为我显然有些不对劲!任何想法将不胜感激! :)
看看GCC或任何其他编译器如何应对它也很有趣!
编辑:Jerry Coffin的想法给了我一些想法。
我已经做了一些测试(在不同的机器上,因此原谅了时间的变化),有没有nops和不同的nops计数
case 2 - 0.46 00401ABD jne (401AB0h)
0 nops - 0.68 00401AB7 jne (401AB0h)
1 nop - 0.61 00401AB8 jne (401AB0h)
2 nops - 0.636 00401AB9 jne (401AB0h)
3 nops - 0.632 00401ABA jne (401AB0h)
4 nops - 0.66 00401ABB jne (401AB0h)
5 nops - 0.52 00401ABC jne (401AB0h)
6 nops - 0.46 00401ABD jne (401AB0h)
7 nops - 0.46 00401ABE jne (401AB0h)
8 nops - 0.46 00401ABF jne (401AB0h)
9 nops - 0.55 00401AC0 jne (401AB0h)
我已经包含了跳转状态,因此您可以看到源和目标位于一个缓存行中。您还可以看到,当我们相隔13个字节或更多时,我们开始有所不同。直到我们达到16 ......然后一切都出错了。
所以Jerry不对(虽然他的建议有点帮助),但是有些事情正在发生。我越来越感兴趣去尝试弄清楚它现在是什么。它确实看起来更像某种内存对齐奇怪而不是某种指令吞吐量奇怪。
任何人都想为一个好奇心灵解释这一点? :d
编辑3:Interjay在展开时有一个点,将之前的编辑从水中吹走。使用展开的循环,性能不会提高。你需要添加一个nop来使跳跃源和目标之间的差距与我上面的好nop计数相同。表现仍然很糟糕。有趣的是,我需要6个小时才能提高性能。我想知道每个周期处理器可以发出多少nops?如果它的3那么说明缓存写入延迟......但是,如果是这样,为什么会发生延迟?
Curiouser和curiouser ......
答案 0 :(得分:3)
我强烈怀疑你所看到的是分支预测的奇怪之处,而不是与缓存有关。特别是,在相当多的CPU上,当分支的源和目标都在同一个缓存行中时,分支预测不起作用(根本就是)。在循环中放入足够的代码(甚至是NOP)以将源和目标放入不同的缓存行中将大大提高速度。
答案 1 :(得分:3)
我和一位英特尔工程师就这个问题进行了简短的交谈,得到了这样的答复:
这显然与哪些指令最终有关 执行单位,机器发现商店重击的速度有多快 问题,以及它如何快速和优雅地处理展开 投机执行以应对它(或者如果它需要多个周期 因为一些内部冲突)。但那说 - 你需要一个非常好的 详细的管道跟踪和模拟器来解决这个问题。预测 这些管道中的无序指令处理太难了 在纸上做,即使是设计机器的人。对于 外行 - 地狱没有希望。遗憾!
Ao我以为我会在这里添加答案并一劳永逸地关闭这个问题:)
答案 2 :(得分:2)
这似乎与编译器无关。起初我认为这可能是由于循环展开等编译器技巧,但是查看生成的程序集,MSVC 9.0只是从C ++代码生成一个直接的转换。
选项1:
$LL3@main:
add DWORD PTR [esi], ecx
add DWORD PTR [esi+4], ecx
add DWORD PTR [esi+8], ecx
add DWORD PTR [esi+12], ecx
sub eax, ecx
jne SHORT $LL3@main
选项2:
$LL3@main:
add DWORD PTR [esi], ecx
add DWORD PTR [esi+8], ecx
add DWORD PTR [esi+16], ecx
add DWORD PTR [esi+24], ecx
sub eax, ecx
jne SHORT $LL3@main
选项3:
$LL3@main:
add DWORD PTR [esi], ecx
add DWORD PTR [esi+8], ecx
sub eax, ecx
jne SHORT $LL3@main
答案 3 :(得分:2)
对于CPU实际执行的操作,x86指令集不再具有代表性。这些说明被翻译成内部机器语言,术语“微操作”在486天内被创造出来。抛出寄存器重命名,推测执行,多个执行单元以及它们与缓存的交互等内容,并且无法预测某些事情应该花多长时间。芯片制造商很久以前就已停止发布周期时间预测。他们的设计是商业秘密。