局部原理及调用说明

Principle of Locality and Call Instructions

在讨论局部性原则时,我的教科书是这样表述的:

Except for branch and call instructions, which constitute only a small fraction of all program instructions, program execution is sequential. Hence, in most cases, the instruction to be fetched immediately follows the last instruction fetched.

作为新手,我觉得这很难相信。我遇到的所有代码都高度填充了调用指令。事实上,在我看来,调用指令实际上执行了程序中最实质性的操作。

尽管调用指令在程序中起着重要作用,但如果有人能详细说明为什么这个概念是正确的,我将不胜感激。

我在我的电脑上随机选择了一个二进制文件,cargo 包管理器。那我:

  • otool -tvV cargo > assembly
  • 反汇编
  • 只得到说明:cat assembly | awk '{print }' > instructions
  • 计算每条指令:sort instructions | uniq -c | sort -n > count

我将 Libreoffice Calc 中的结果处理成每条指令出现的列表。以下是每个占程序 1% 以上的操作(这些总和高达 86%,因此我为了 brewity 而丢弃了大量的杂散操作):

| 34.83% | movq    |
| 7.30%  | leaq    |
| 7.00%  | callq   |
| 6.90%  | je      |
| 5.61%  | movl    |
| 4.86%  | cmpq    |
| 3.77%  | testq   |
| 3.11%  | jmp     |
| 2.23%  | jne     |
| 2.17%  | popq    |
| 2.05%  | pushq   |
| 1.69%  | addq    |
| 1.29%  | cmpl    |
| 1.20%  | movabsq |
| 1.18%  | movb    |
| 1.05%  | xorl    |

这里肯定有很多分支和调用(callqjmpjejne),但也有很多内存操作。内存操作相对较慢,占程序运行时间的很大一部分。 movq只是一个内存操作,却占了程序的三分之一以上!

CPU 缓存用于将最近引用的内存数据保持在靠近 CPU 核心的位置,从而加快以后对相同数据的内存操作。他们之所以可以这样做,是因为 Principle of Locality 表明对同一内存的操作通常会在时间上接近(时间局部性)。所以可以缓存内存数据,因为您可能很快就会再次需要它。