MSVC下的奇数优化问题

时间:2010-02-05 13:01:27

标签: c++ optimization assembly x86

我见过这个博客:

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 ......

4 个答案:

答案 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天内被创造出来。抛出寄存器重命名,推测执行,多个执行单元以及它们与缓存的交互等内容,并且无法预测某些事情应该花多长时间。芯片制造商很久以前就已停止发布周期时间预测。他们的设计是商业秘密。