1

我有两种方法来编写相同的功能。

方法一:

doTheWork(int action)
{
    for(int i = 0 i < 1000000000; ++i)
    {
        doAction(action);
    }
}

方法二:

doTheWork(int action)
{
    switch(action)
    {
    case 1:
        for(int i = 0 i < 1000000000; ++i)
        {
            doAction<1>();
        }
        break;
    case 2:
        for(int i = 0 i < 1000000000; ++i)
        {
            doAction<2>();
        }
        break;
    //-----------------------------------------------
    //... (there are 1000000 cases here)
    //-----------------------------------------------
    case 1000000:
        for(int i = 0 i < 1000000000; ++i)
        {
            doAction<1000000>();
        }
        break;
    }
}

假设函数doAction(int action)和函数template<int Action> doAction()由大约 10 行代码组成,这些代码将在编译时内联。调用doAction(#)等效doAction<#>()于功能,但非模板化doAction(int value)的速度比 慢一些template<int Value> doAction(),因为在编译时已知参数值时可以在代码中进行一些很好的优化。

所以我的问题是,在模板化函数的情况下,是否所有数百万行代码都填充了 CPU L1 缓存(以及更多)(从而大大降低了性能),还是只有doAction<#>()当前正在运行的循环内部的行得到缓存?

4

2 回答 2

2

这取决于实际的代码大小——10 行代码可以少也可以多——当然也取决于实际的机器。

然而,方法 2 严重违反了这几十年的经验法则:指令便宜,内存访问不便宜。

可扩展性限制

您的优化通常是线性的 - 您可能会减少 10、20 甚至 30% 的执行时间。达到缓存限制是高度非线性的——就像“撞到砖墙”一样是非线性的。

一旦您的代码大小显着超过 2 级/3 级缓存的大小,方法 2 就会浪费大量时间,正如以下对高端消费者系统的估计所示:

  • 10667MB/s具有峰值内存带宽的DDR3-1333 ,
  • 英特尔酷睿 i7 Extreme 与 ~75000 MIPS

每条指令为您提供 10667MB / 75000M = 0.14 字节以实现收支平衡 - 任何更大的内存都无法跟上 CPU 的速度。

典型的 x86 指令大小是 2..3 字节,在 1..2 个周期内执行(现在,当然,这不一定是相同的指令,因为 x86 指令被拆分。仍然......)典型的 x64 指令长度甚至更大.

你的缓存有多大帮助?
我找到了以下数字(来源不同,因此很难比较):i7 Nehalem L2 缓存(256K,>200GB/s 带宽)几乎可以跟上 x86 指令,但可能跟不上 x64。

此外,只有在以下情况下,您的 L2 缓存才会完全启动

  • 您对下一条指令有完美的预测,或者您没有首次运行惩罚,它完全适合缓存
  • 没有大量数据正在处理
  • 您的“内部循环”中没有重要的其他代码
  • 在这个核心上没有执行线程

鉴于此,您可能会更早丢失,尤其是在具有较小缓存的 CPU/板上。

于 2010-07-28T21:41:13.983 回答
1

L1 指令高速缓存将仅包含最近获取的指令或预期在不久的将来执行的指令。因此,第二种方法不能仅仅因为代码在那里就填充 L1 缓存。您的执行路径将导致它加载代表正在运行的当前循环的模板实例化版本。当您进入下一个循环时,它通常会使最近最少使用 (LRU) 缓存行无效,并将其替换为您接下来执行的内容。

换句话说,由于这两种方法的循环性质,L1 缓存在这两种情况下都会表现出色,不会成为瓶颈。

于 2010-07-27T17:31:49.353 回答