【发布时间】:2010-02-05 13:01:26
【问题描述】:
我看过这个博客:
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 个周期(用于缓存)然后 p[3](1 个周期)。最后 2 个循环用于递增和跳转(虽然它通常实现为递减和跳转)。总共有 8 个周期。
在选项 2 中:它可以在一个循环中增加 p[0] 和 p[4],然后在另一个循环中增加 p[2] 和 p[6]。然后 2 个循环进行减法和跳转。无需等待缓存。总共 4 个周期。
在选项 3 中:它可以递增 p[0],然后必须等待 2 个周期,然后递增 p[2],然后减去并跳转。问题是,如果您将案例 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 个 nop 来提高性能。我想知道处理器每个周期可以发出多少个 nop?如果它是 3,则说明缓存写入延迟......但是,如果是这样,为什么会出现延迟?
好奇者和好奇者 ...
【问题讨论】:
-
FWIW,很容易让 GCC 在几乎任何操作系统上运行以进行比较,并且您可以免费获得英特尔的编译器。在 Ubuntu 上安装 icc 对我来说非常简单,只要记住你必须有一个 Intel 芯片才能利用它的优化。
-
我唯一能想到的是一些指令调度怪癖。由于循环较短,CPU 可能不得不在迭代之间暂停几个周期以等待写入完成,这必须由于某种原因导致 额外 减速,使其比较长的循环慢。缓存延迟似乎会平等地影响所有情况,就像您说的那样,R/W 端口宽度似乎也不是。我能想象到的唯一可能导致较短循环花费更长的因素是 CPU 中的某种调度限制。
-
@Jalf:GCi32 是有符号的 32 位整数。对不起,我应该说清楚。我已经编辑了代码。
-
对我来说,一个危险信号是你使用 double 来计时整个 sha-bang。您应该使用 QueryPerformanceTimer 并使用整数数据类型来存储时间。
-
@Andreas:这有什么关系?
标签: c++ optimization assembly x86