【问题标题】:Is CPU speed limited by the speed of fetching instructions from memory?CPU 速度是否受从内存中获取指令的速度限制?
【发布时间】:2017-08-22 13:33:51
【问题描述】:

在学习汇编时,我意识到我应该将经常访问的数据放在寄存器而不是内存中,因为内存要慢得多。

问题是,既然指令首先是从内存中获取的,那么 CPU 怎么能比内存运行得更快呢? CPU通常会花费大量时间等待来自内存的指令吗?

编辑: 要运行程序,我们需要将其编译为包含机器代码的文件。然后我们将该文件加载到内存中,并一条接一条地运行指令。 CPU 需要知道要运行什么指令,并且该信息是从内存中获取的。我不是在问如何操作数据,而是在问从内存中读取指令的过程。对不起,如果我不够清楚。

编辑 2

示例:xor eax, eax 在我的计算机上编译为 31c0。我知道这个指令本身很快。但是要清除eax,CPU 需要先从内存中读取31c0。如果访问内存很慢,那么读取应该会花费很多时间,而在此期间 CPU 会停止?

【问题讨论】:

  • 如果这是题外话,请让我知道我应该在哪里问。
  • 您可以添加示例代码和基准测试以使其更符合主题
  • @raam86 我真的不知道如何进行基准测试。
  • @TheoYou 在我的答案中尝试代码。记得关闭优化。
  • @klutt 请仔细看看我的例子。我不是在问如何操作内存中的数据。我不是以英语为母语的人,如果我能说得清楚一点,我很抱歉。

标签: performance memory cpu cpu-architecture


【解决方案1】:

与指令执行并行的代码提取非常重要,甚至 8086 也做到了(在有限的范围内,预取缓冲区非常小且带宽低)。即便如此,代码获取带宽实际上是 8086 的主要瓶颈。

(我刚刚意识到你没有标记这个 x86,尽管你确实使用了 x86 指令作为示例。我所有的示例都是 x86,但是对于任何其他架构来说,基础几乎都是一样的。除了非 x86 CPU 不会使用解码的 uop 缓存,x86 是唯一仍在使用的 ISA,它很难解码,值得缓存解码结果。)


在现代 CPU 中,代码获取很少成为瓶颈,因为 caches 和预取隐藏了延迟,并且与数据所需的带宽相比,带宽要求通常较低。 (不过,占用大量代码的臃肿代码可能会因指令缓存未命中而导致速度变慢,从而导致前端停滞。)

L1I 缓存与 L1D 缓存是分开的,CPU 每个周期提取/解码至少 16 字节的 x86 代码块。带有解码微指令缓存(Intel Sandybridge 系列和 AMD Ryzen)的 CPU 甚至可以缓存已解码的指令以消除解码瓶颈。

请参阅http://www.realworldtech.com/sandy-bridge/3/,了解有关英特尔 Sandybridge 前端的相当详细的文章(获取/预解码/解码/重命名+问题),以及类似这样的框图,展示了英特尔 Sandybridge 与英特尔 Nehalem 和AMD Bulldozer 的指令获取逻辑。 (解码在下一页)。 “预解码”阶段找到指令边界(即在解码每条指令的实际内容之前解码指令长度)。

L1I 缓存未命中会导致对统一 L2 的请求。现代 x86 CPU 还具有共享的 L3 缓存(在多个内核之间共享)。

硬件预取将即将需要的代码带入 L2 和 L1I,就像将数据预取入 L2 和 L1D 一样。这在大多数情况下隐藏了 DRAM > 200 个周期的延迟,通常只会在跳转到“冷”功能时失败。在运行没有采用分支的长序列代码时,它几乎总是可以保持领先于解码/执行,除非其他事情(如数据加载/存储)正在耗尽所有内存带宽。

您可以构建一些以每周期 16 字节解码的代码,这可能高于主存带宽。或者在 AMD CPU 上甚至更高。但通常解码瓶颈会限制您超过纯代码获取带宽。


另请参阅Agner Fog's microarch guide,了解有关各种微架构中的前端以及针对它们优化 asm 的更多信息。

另请参阅 标签 wiki 中的其他 CPU 性能链接。

【讨论】:

    【解决方案2】:

    如果您经常访问数据,很可能您也有相同的指令重复处理它们。高效的 CPU 不会一次又一次地从慢速内存中获取相同的指令。相反,它们被放置在一个访问时间非常短的指令缓存中。因此,cpu一般不需要等待指令。

    【讨论】:

    • 如果我的程序没有跳转,每条指令都执行一次怎么办?
    • 这是一个不太可能发生的特殊情况。尽管如此,指令将由 block 获取,而不是一条一条地获取,这最终将最小化指令获取的平均访问时间。
    • @TheoYou:硬件预取到指令缓存通常可以保持领先于执行,隐藏了代码获取的延迟。有关更多详细信息,请参阅我的答案。
    【解决方案3】:

    与 CPU 相比,内存非常慢。从 RAM 中获取数据大约需要 200 个时钟周期,因此通常编写缓存友好代码对性能非常重要。是的,CPU 花费大量时间等待数据。

    为什么会这样?嗯,这只是不同种类的记忆。一般来说,创建快速内存的成本更高,因此为了降低成本,为寄存器保留最快的内存。物理距离也可能是速度的限制。您想要快速访问的内存需要靠近核心。光以大约 300 000 公里/秒的速度传播,这意味着大约 0.3 毫米/纳秒。如果内存在 0.3 毫米之外,物理上不可能在一纳秒内获取数据。 RAM 通常距离 10 厘米,因此在大约 30 纳秒内无法访问。现代 CPU:s 以 GHz 的频率工作,因此我们已经达到了不可能(不难,不可能)使内存跟上 CPU 的障碍。

    但是,这种物理限制(相对论)只影响访问时间,而不影响带宽。因此,当您在地址 addr 获取数据时,也无需花费任何额外费用来获取 addr+1

    在寄存器和 RAM 之间有缓存。在现代计算机中,它通常是三层缓存。这与将硬盘驱动器中的数据缓存在 RAM 中时的工作方式类似。当你读取一点数据时,很可能你很快就会需要周围的数据,所以同时读取周围的数据并存储在缓存中。当你请求下一条数据时,它很可能在缓存中。每当您从内存中请求某些内容时,都会有电路检查该内存是否已存在于缓存中。

    您无法直接控制缓存。您可以做的是编写缓存友好的代码。对于高级情况,这可能会很棘手,但总的来说,诀窍是不要在内存中跳跃很长的距离。尝试顺序访问内存。

    这里是一个如何编写缓存友好的简单示例:

    int *squareMatrix=malloc(SIZE*SIZE*sizeof(*squareMatrix));
    int sum=0;
    for(int i=0; i<SIZE; i++) 
       for(int j=0; j<SIZE; j++) 
          sum+=squareMatrix[i*SIZE+j];
    

    还有一个非缓存友好的版本:

    int *squareMatrix=malloc(SIZE*SIZE*sizeof(*squareMatrix));
    int sum=0;
    for(int i=0; i<SIZE; i++) 
       for(int j=0; j<SIZE; j++) 
          sum+=squareMatrix[j*SIZE+i];
    

    区别在于[j*SIZE+i][i*SIZE+j]。第一个版本顺序读取整个矩阵,大大增加了下一个元素在您请求时已经在内存中的机会。

    这是我电脑上上面代码与SIZE=30000的区别:

    $ time ./fast
    
    real    0m2.755s
    user    0m2.516s
    sys     0m0.236s
    
    $ time ./slow
    
    real    0m18.609s
    user    0m18.268s
    sys     0m0.340s
    

    如您所见,这会显着影响性能。

    不同类型内存的典型访问时间和大小。非常近似,只是为了大致了解一下:

    Memory type           # Clock tics     Size
    ===================|================|=============
    register           |             1  |    8B each, around 128B total
    level1 cache       |             5  |    32kB
    level2 cache       |            10  |    1MB
    level3 cache       |            50  |    20MB
    RAM                |           200  |    16GB
    SSD drive          |        10,000  |    500GB
    Mechanical drive   |     1,000,000  |    4TB
    

    还可以提到,一级缓存通常分为数据和代码。

    【讨论】:

    • 获取 addr+1 不需要任何额外费用:不完全正确。它需要一个额外的缓冲区来跟踪一个额外的未完成的内存请求。内存并行度受缓冲区数量的限制。例如对于数据,行填充缓冲区的数量限制了单核memset 带宽,以及为什么具有更高延迟 L3 / RAM 的大型 Xeon 的单核带宽低于四核台式机 CPU,即使使用多核活动至强具有更高的聚合带宽。请参阅stackoverflow.com/questions/43343231/…中的“延迟绑定平台”
    • 这个答案的一个问题是它甚至没有提到 L1I 缓存与 L1D 缓存是分开的,并且有一个单独的 iTLB。您的示例是一种数据访问模式,而不是跳来跳去的代码。 (当然,分支预测限制会使这变得复杂,但您可以在遇到分支错误预测之前在 L1I 中执行一些导致冲突丢失的操作,因为它在英特尔 Sandybridge 系列中只有 8 路关联。)
    猜你喜欢
    • 2010-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-18
    • 1970-01-01
    • 2013-06-14
    • 1970-01-01
    相关资源
    最近更新 更多