当我以前编写嵌入式系统和早期的8/16位PC(6502,68K,8086)时,我非常好地处理了每条指令执行的时间(以纳秒或微秒为单位).根据系列,一个(或四个)周期等同于一个"内存提取",并且无需担心缓存,您可以根据所涉及的内存访问次数猜测时序.
但是对于现代CPU,我很困惑.我知道它们的速度要快得多,但我也知道,如果不知道每条指令需要多少个时钟周期,标题千兆赫速度就无济于事.
那么,任何人都可以为两个示例指令提供一些时序,比如说(2)Core 2 Duo.最好和最坏的情况(假设缓存中没有任何内容/缓存中的所有内容)将是有用的.
指令#1:将一个32位寄存器添加到第二个.
指令#2:将32位值从寄存器移到存储器.
编辑:我之所以要这样做是为了尝试开发一个"经验法则",这样我就可以查看简单的代码并粗略估计所需的时间到最接近的数量级.
编辑#2:有趣点的答案很多,但没有人(还)已经记下了及时测量的数字.我很欣赏这个问题有"复杂性",但是来吧:如果我们可以估算出纽约市钢琴调音师的数量,我们应该能够估计代码运行时间......
采取以下(哑)代码:
int32 sum = frigged_value(); // start timing for (int i = 0 ; i < 10000; i++) { for (int j = 0 ; j < 10000; j++) { sum += (i * j) } sum = sum / 1000; } // end timing
我们如何估计运行多长时间... 1飞秒?1千兆?
你提到的现代处理器如Core 2 Duo都是超标量和流水线.它们每个核心有多个执行单元,实际上每个核心一次处理多个指令; 这是超标量部分.流水线部分意味着从读取和"发出"指令到完成执行时存在延迟,并且该时间根据该指令与同时移动通过其他执行单元的其他指令之间的依赖性而变化.因此,实际上,任何给定指令的时序取决于它周围的内容和依赖的内容.这意味着给定指令具有基于许多因素的最佳情况和最差情况执行时间.由于多个执行单元,您实际上可以有多个指令完成每个核心时钟的执行,
以上所有内容都是从CPU内核本身的角度出发的.然后,您与缓存进行交互,并与其他核心争用带宽.CPU 的总线接口单元处理将指令和数据输入到内核中,并通过高速缓存将结果从内核返回到内存.
用一粒盐做的粗略的数量级经验法则:
注册到寄存器操作需要1个内核时钟才能执行.这通常应该是保守的,特别是因为更多这些按顺序出现.
与内存相关的加载和存储操作需要1个内存总线时钟来执行.这应该是非常保守的.具有高缓存命中率,它将更像是2个CPU总线时钟,它是CPU内核和缓存之间总线的时钟速率,但不一定是核心时钟.
几乎不可能以对您有用的方式提供您期望的准确计时信息.
以下概念影响指令时序; 有些可能随时变化:
微操作分解
操作流水线
超标量执行
乱序执行
SMT/SMP执行
浮点模式
分支预测/预取
缓存延迟
内存延迟
时钟速度限制
等等
如果您需要有关上述概念的任何进一步说明,请查阅有关现代计算机体系结构的书
测量代码速度的最佳方法是(惊喜!)来衡量运行相同工作负载的代码的速度,以及在"在现实世界中"时所处的相同条件.
使用主要基于英特尔奔腾架构的描述来缩短一个非常长的故事:
处理器有许多"执行单元",可以执行不同类型的"微操作"; 指令可以分成几个微操作
不同的执行单元基本上并行运行
每个微操作将相应的执行单元绑定一定数量的时钟周期,因此同时没有其他指令可以使用该执行单元:例如,"浮点加"可以将"FP执行"单元占用2个时钟周期
执行单元按"端口"分组,每个时钟周期,一个新的微操作可以发送到每个端口(假设相关的执行单元在那一刻是空闲的); 一些单位也可以在周期中途发送"额外操作"; 所以每个时钟周期,一定数量的操作可以开始执行;
处理器可以重新排序微操作,其中这不会破坏依赖性(或者仍然可以重建结果)以利用哪些执行单元在给定时刻是空闲的
所以指令可以并行执行,但是任何一次执行哪些指令部分都是非常复杂的情况
因此,给定指令的总时间取决于必须"等待"必要的执行单元变得可用的时间,这些操作在给定单元上运行的实际时间,以及"绑定"所需的额外时间.结果"
由于指令的时间取决于周围的指令,因此在实践中,通常最好对代表性的代码进行计时而不是尝试并担心单个指令.然而:
英特尔(可能是其他制造商)发布了指令吞吐量和延迟时序列表
的吞吐量是实际需要的相关执行单元上的时钟周期的数目(S)
一旦指令开始执行,在执行结果可用作另一条指令的输入之前,延迟是所需时钟周期的"最坏情况"数
因此,例如,如果,例如,浮点加法和乘法指令的吞吐量均为2且延迟为5(实际上,为了将其加倍,我认为),这意味着向自身添加寄存器或将其乘以本身可能需要两个时钟周期(因为没有其他相关值),而添加它之前的乘法结果将需要或稍微少于2 + 5个时钟周期,具体取决于您开始/结束时序的位置,以及在各种其他事情上.(在某些时钟周期中,可能会发生另一个加/复运算,因此可以说你实际上有多少个周期归属于单个add/mutliply指令......)
哦,就像一个具体的例子.用于遵循Java代码
public void runTest(double[] data, double randomVal) { for (int i = data.length-1; i >= 0; i--) { data[i] = data[i] + randomVal; } }
Hotspot 1.6.12 JIT-将内循环序列编译为以下英特尔代码,包括数组中每个位置的加载 - 存储(在这种情况下,'randomVal'保存在XMM0a中):
0b3 MOVSD XMM1a,[EBP + #16] 0b8 ADDSD XMM1a,XMM0a 0bc MOVSD [EBP + #16],XMM1a 0c1 MOVSD XMM1a,[EBP + #8] 0c6 ADDSD XMM1a,XMM0a 0ca MOVSD [EBP + #8],XMM1a ...
每组加载 - 添加存储似乎需要5个时钟周期.
这不是那么简单.两条指令的时间安排不会帮助您更好地衡量更多指令的性能.这是因为现代处理器可以并行执行许多操作,并且具有大型缓存,因此"将值移动到内存"发生在与指令执行完全相同的时间.
因此,最佳情况为零(与其他指令并行执行时).但这对你有什么帮助?
该网页显示了一些基准测试,包括一些%MIPS/MHz结果.如您所见,在许多基准测试中,每个时钟周期执行多条指令.图表还显示了缓存大小和内存速度的影响.
现代处理器做的事情更棘手.
乱序执行.如果可以在不影响正确行为的情况下执行此操作,则处理器可以按照与程序中列出的顺序不同的顺序执行指令.这可以隐藏长时间运行指令的延迟.
注册重命名.处理器通常在其指令集中具有比可寻址寄存器更多的物理寄存器(所谓的"架构"寄存器).这可以是为了向后兼容,或者只是为了实现有效的指令编码.当程序运行时,处理器将"重命名"它使用的架构寄存器,以便任何物理寄存器都是空闲的.这允许处理器实现比原始程序中存在的更多的并行性.
例如,如果您在EAX和ECX上有很长的操作序列,然后是将EAX和ECX重新初始化为新值并执行另一个长序列操作的指令,则处理器可以为这两个任务使用不同的物理寄存器,并执行他们并行.
英特尔P6微架构同时执行无序执行和寄存器重命名.Core 2架构是P6的最新衍生产品.
要真正回答您的问题 - 面对所有这些架构优化,您基本上无法手动确定性能.
你所要求的那种预测是没有希望的.
如果你想要一个经验法则,这里有一些经验法则:
在从2级缓存中获取单词所需的时间内,处理器可以执行至少10条指令.所以担心内存访问,而不是指令计数---寄存器中的计算几乎是免费的.
在从RAM中获取字的时间内,处理器可以执行数千条指令(根据硬件的详细信息,这个数字会变化几个数量级).确保只在冷缓存上发生这种情况; 否则没有其他问题.
如果您在x86 CPU上运行,则没有足够的寄存器.尽量不要在代码中包含超过5个实时变量.或者更好的是,转移到AMD64(x86_64
)并将寄存器数量增加一倍.有16个寄存器和寄存器中传递的参数,您可以放弃担心寄存器.
曾经有一段时间我会问建筑师我应该使用哪些经验法则来预测编译器生成的代码的成本.我已经停止了,因为上次我收到一个有用的答案是在1999年.(答案是"确保你的循环适合重新排序缓冲区".所有知道重新排序缓冲区的人现在都可以举手了.如果您可以在当前使用的任何计算机上发现重新排序缓冲区的大小,请指向.)
这只回答了你的部分问题,但是我发现维基百科的这张表在参考地点很有帮助.它使用大约2006次来描述内存层次结构的不同级别的访问速度和内存量:
CPU寄存器(8-32个寄存器) - 立即访问(0-1个时钟周期)
L1 CPU缓存(32 KiB至128 KiB) - 快速访问(3个时钟周期)
L2 CPU缓存(128 KiB至12 MiB) - 访问速度稍慢(10个时钟周期)
主物理内存(RAM)(256 MiB至4 GiB) - 访问速度慢(100个时钟周期)
磁盘(文件系统)(1 GiB到1 TiB) - 非常慢(10,000,000个时钟周期)
远程内存(如其他计算机或Internet)(实际上无限制) - 速度各不相同