【问题标题】:Is memory access slower than shifting? [closed]内存访问比移位慢吗? [关闭]
【发布时间】:2021-02-24 19:50:16
【问题描述】:

以下两个代码 sn-ps 中哪一个通常会运行得更快(没有编译器优化)?这段代码只是一个例子——我知道有更快的方法来做同样的计算。

// arr points to the following array: [1,2,4,8,16,32,64]
// assume that it has already been created, so that the
// array creation does not cause a time penalty
int result = 0;
for (int i = 0; i < 7; i++) {
    result += arr[i];
}
int result = 0;
for (int i = 0; i < 7; i++) {
    result += (1 << i);
}

我很确定内存访问会更慢,但我想确认一下。

编辑:为了澄清这个问题,我对此代码的未优化版本特别感兴趣。不是因为我真的打算在生产中使用这段代码,而是因为我对内存访问或算术是否更快的概念感兴趣。也许我应该用汇编语言而不是 C/C++ 编写代码,以进一步说明我对代码的编译器优化不感兴趣。

一些回复说在大多数情况下内存访问速度较慢,而其他人则说我应该进行基准测试,这取决于我的处理器和系统。

感兴趣的架构是 x86 或 x86-64 - 您可以在 2021 年在现代笔记本电脑或台式计算机中找到这些架构。一旦我在未优化和优化版本上运行了一些基准测试,我将再次编辑这个问题代码。感谢到目前为止所有回复的人。

编辑 2:我在上面两个代码 sn-ps 的变体上运行 gprof,发现平均(经过数十亿次运行后),使用内存访问的计算版本大约需要 2.4 纳秒,而使用内存访问的版本大约需要 2.4 纳秒。使用算术大约需要 1.1 纳秒。这是在 64 位 Linux 计算机上。

运行未优化 (-O0) 并使用 GCC 版本 10.2.0。我还尝试使用 clang 版本 10.0.1,结果如下:内存版本平均为 2.45 纳秒(与 GCC 没有显着差异),算术版本为 1.63 纳秒(明显比 GCC 差,尽管这种差异可能有其他原因......我的基准并不严格,因为它只是为了给出一个粗略的估计)。

我用于基准测试的变体将循环替换为一系列 7 行重复的代码(result += arr[0]; ...result += (1 &lt;&lt; 0); ...),因为我发现循环本身比两种情况下的计算花费的时间要多得多。

我没有为优化运行而烦恼,因为我知道算术版本将优化为单个常量(127 或 0x7F),它实际上只是测试编译器是否足够聪明以优化内存版本。

【问题讨论】:

  • 如果您想知道 X 是否比 Y 快,请使用 benchmarker
  • 在任何现代处理器上(1990 年左右之后),几乎可以肯定的是,内存访问会比移位慢得多。
  • "...没有编译器优化" - 谁在乎未优化的代码?
  • “没有编译器优化”不仅无趣,而且毫无意义。对于“最佳”的某些特定定义,至少可以将优化的代码视为编译器知道如何生成的最佳代码,但谁能说这与同一编译器可能生成的代码有何不同?。
  • @Agent008 如果您的原始代码无法以相同的方式进行优化,那么对这个问题的任何回答对您来说都是无用的。您必须同时衡量问题、编译器和系统的组合才能得到答案。

标签: c++ c optimization


【解决方案1】:

我建议你看看Agner Fog的优秀手册,尤其是one related to C++

优化速度是相关的 当 CPU 访问和内存访问是关键时间消耗者时。

在大多数现代架构中,移位(ALU 指令)将比内存访问快得多,引用手册:

移位操作(1 个时钟周期)

在大多数微处理器上,移位操作只需要一个时钟周期

内存访问(2 到 4 个时钟周期,或更差)

从 RAM 内存中访问数据可能需要相当长的时间,而不是它所花费的时间 对数据进行计算。这就是为什么所有现代计算机都有内存的原因 缓存。通常,一级数据缓存为 8 - 64 KB,二级缓存为 256 千字节到 2 MB。通常,还有几兆字节的三级缓存。

程序中所有数据的总大小大于二级缓存和数据 分散在内存中或以非顺序方式访问,那么很可能 内存访问是程序中最大的耗时。 读取或写入 内存中的变量如果缓存起来只需要2-4个时钟周期,但是几百个时钟 如果没有缓存则循环。请参阅第 25 页关于数据存储和第 89 页关于内存 缓存。

所以在你的具体例子中,你应该去换班。

【讨论】:

  • 当 CPU 访问和内存访问是关键时间消耗者时,速度优化是相关的。注意粗体部分。在您分析您的应用程序并真正确定性能瓶颈之前,您几乎肯定是在浪费时间尝试“眼球优化”代码。首先,您认为看起来更快的东西与优化编译器将变成运行速度更快的指令完全无关。其次,您几乎可以肯定在需要优化的方面错误。如果您还没有分析您的应用程序,那么您还没有确定关键部分。
【解决方案2】:

因为您的问题似乎是关于从记忆中加载单词的相对速度vs。执行算术运算,您似乎真的要求进行比较,更像是评估

*p

对比

p + 1

,其中p 是指向int 的指针,其值本身不需要从内存中获取(因为它已经在CPU 寄存器中)。一般来说,现代 CPU 的算术单元的运行速度至少比附加的内存子系统快几倍,即使对于内容当前可从 CPU 最快的缓存中获得的内存位置也是如此,因此人们通常会期望后者更快。

但是以这种粒度来查看性能问题确实没有意义,尤其是不要询问未优化的代码。包含此类操作的整个程序的性能在很大程度上受操作上下文的影响,如果您正在寻求最佳性能,那么您当然会编译 with 优化,而不是没有。此外,如果在任何给定情况下完全不同的话,通常完全不清楚在没有启用优化的情况下生成的代码与在启用优化的情况下生成的代码有何不同。

总的来说,这个问题有一种过早优化的味道——即程序员的微优化,而不是编译器优化——这通常会适得其反。为获得最佳性能,请针对问题使用最佳算法,并编写干净、自然的代码。这通常会帮助编译器在优化方面做得更好,并且肯定会让你的代码更容易调试和维护。如果生成的程序不够快,则 profile 确定最大的瓶颈在哪里,然后解决这些问题。

【讨论】:

    【解决方案3】:

    Duff's Device 包含在比较中也会很有趣。它使用文章中提到的 loop unrolling 来通过减少循环数来减少复制所需的指令。

    "循环展开的基本思想是可以通过减少循环次数来减少循环中执行的指令数 循环测试,有时会减少在循环测试中花费的时间 环形。例如,在只有一个循环的情况下 块代码中的指令,循环测试通常是 为循环的每次迭代执行,即每次 指令被执行。相反,如果相同的八个副本 指令放在循环中,然后将执行测试 仅每八次迭代,这可能会通过避免七次迭代来获得时间 测试。但是,这只处理八次迭代的倍数, 需要其他东西来处理任何剩余的迭代。”

    Duff's Device的示例代码sn-p:

    void copy_duff(register short *to, register short *from, register  count)
    {
        register n=(count+7)/8;
        switch(count%8) {
            case 0:    do {    *to = *from++;
            case 7:        *to = *from++;
            case 6:        *to = *from++;
            case 5:        *to = *from++;
            case 4:        *to = *from++;
            case 3:        *to = *from++;
            case 2:        *to = *from++;
            case 1:        *to = *from++;
            } while(--n>0);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2010-10-14
      • 1970-01-01
      • 1970-01-01
      • 2017-10-15
      • 2012-04-10
      • 2021-07-12
      • 2015-02-12
      • 2016-11-14
      • 1970-01-01
      相关资源
      最近更新 更多