【问题标题】:Does this code fill the CPU cache?这段代码是否填满了 CPU 缓存?
【发布时间】:2010-07-20 07:52:51
【问题描述】:

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

方法一:

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&lt;int Action&gt; doAction() 由大约10 行代码组成,这些代码将在编译时内联。调用doAction(#) 在功能上等同于doAction&lt;#&gt;(),但非模板化的doAction(int value) 比template&lt;int Value&gt; doAction() 慢一些,因为在编译时参数值已知的情况下,可以在代码中进行一些很好的优化。

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

【问题讨论】:

  • “让我们假设函数 doAction(int action) 和函数模板 doAction() 包含大约 10 行代码,这些代码将在编译时内联。”编译器通常不够聪明,无法优化您的模板版本,切换会比您的 L1 缓存更严重地降低您的性能
  • 我明白你的意思是这么大的开关是那里性能的最大问题。但我最感兴趣的是缓存 L1 是否在第二种方法中被填满并且有点问题,或者根本没有问题。我只是不知道它是如何工作的。我还想知道为什么编译器不知道如何内联函数的模板版本,假设它非常相似但比常规函数简单一点?

标签: optimization cpu-cache


【解决方案1】:

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

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

可扩展性限制

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

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

  • DDR3-1333 与 10667MB/s 峰值内存带宽,
  • Intel Core i7 Extreme with ~75000 MIPS

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

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

    • 假设 doAction&lt;int&gt; 实例没有再次折叠,代码需要从 L2/L3/主内存中获取。以典型的内存/执行周期比率,即使您完美地预测接下来会出现什么指令,您也无法足够快地将它们铲入 L1 缓存。
    猜你喜欢
    • 2015-05-26
    • 2019-07-09
    • 1970-01-01
    • 1970-01-01
    • 2012-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-05
    相关资源
    最近更新 更多