过去我曾经处理过时间关键的软件开发。这些应用程序的开发基本上是这样进行的:“让我们编写代码,测试延迟和抖动,并优化两者,直到它们在可接受的范围内。” 我觉得这非常令人沮丧;这不是我所说的合适的工程,我想做得更好。
所以我研究了一个问题:为什么我们会有抖动?当然,答案是:
缓存:从主存中获取代码或数据比从L1缓存中获取相同的数据需要多出2个数量级的时间。因此,物理执行时间取决于缓存中的内容。而这又取决于几个因素:
应用程序的代码和数据布局:我们都知道可怕的行对列主矩阵遍历示例
CPU的缓存策略,包括缓存行的推测性预取
同一核心上的其他进程正在执行任务
分支预测:CPU试图猜测哪个条件跳转的分支将被执行。即使相同的条件跳转被执行两次,预测也可能不同,因此有时会形成“流水线气泡”,有时则不会。
中断:异步行为显然会导致抖动
频率缩放:在实时系统中已经禁用
这是很多可以干扰代码行为的东西。尽管如此:如果我有两个指令,位于同一个缓存行上,不依赖任何数据,也不包含(条件)跳转。那么缓存和分支预测的抖动应该被消除了,只有中断应该起作用。对吗?好吧,我编写了一个小程序两次获取时间戳计数器(tsc),并将差异写入stdout。我在一个rt-patched linux内核上执行它,并禁用了频率缩放。
该代码具有基于glibc的初始化和清除,调用printf,我认为有时在缓存中,有时不在。但在“rdtsc”(将tsc写入edx:eax)之间的所有内容都应该在每次执行二进制文件时都是确定性的。只是为了确保,我反汇编了elf文件,这里是两个rdtsc调用的部分:
没有条件跳转,位于同一缓存行(虽然我不能百分之百确定 - ELF 加载器确切地将指令放在哪里?这里的 64 字节边界是否映射到内存中的 64 字节边界?)... 这个抖动是从哪里来的?如果我通过 zsh 执行该代码 1000 次(每次重新启动程序),我得到的值从 12 到 46 不等,并有几个中间值。由于我的内核已禁用频率缩放,因此只剩下中断。现在我愿意相信,在 1000 次执行中,会有一次被中断。但我不愿相信有 90% 被中断了(我们正在讨论 ns 级别的时间间隔!那么中断应该来自哪里呢?!)。
所以,我的问题如下:
所以我研究了一个问题:为什么我们会有抖动?当然,答案是:
缓存:从主存中获取代码或数据比从L1缓存中获取相同的数据需要多出2个数量级的时间。因此,物理执行时间取决于缓存中的内容。而这又取决于几个因素:
应用程序的代码和数据布局:我们都知道可怕的行对列主矩阵遍历示例
CPU的缓存策略,包括缓存行的推测性预取
同一核心上的其他进程正在执行任务
分支预测:CPU试图猜测哪个条件跳转的分支将被执行。即使相同的条件跳转被执行两次,预测也可能不同,因此有时会形成“流水线气泡”,有时则不会。
中断:异步行为显然会导致抖动
频率缩放:在实时系统中已经禁用
这是很多可以干扰代码行为的东西。尽管如此:如果我有两个指令,位于同一个缓存行上,不依赖任何数据,也不包含(条件)跳转。那么缓存和分支预测的抖动应该被消除了,只有中断应该起作用。对吗?好吧,我编写了一个小程序两次获取时间戳计数器(tsc),并将差异写入stdout。我在一个rt-patched linux内核上执行它,并禁用了频率缩放。
该代码具有基于glibc的初始化和清除,调用printf,我认为有时在缓存中,有时不在。但在“rdtsc”(将tsc写入edx:eax)之间的所有内容都应该在每次执行二进制文件时都是确定性的。只是为了确保,我反汇编了elf文件,这里是两个rdtsc调用的部分:
00000000000006b0 <main>:
6b0: 0f 31 rdtsc
6b2: 48 c1 e2 20 shl $0x20,%rdx
6b6: 48 09 d0 or %rdx,%rax
6b9: 48 89 c6 mov %rax,%rsi
6bc: 0f 31 rdtsc
6be: 48 c1 e2 20 shl $0x20,%rdx
6c2: 48 09 d0 or %rdx,%rax
6c5: 48 29 c6 sub %rax,%rsi
6c8: e8 01 00 00 00 callq 6ce <print_rsi>
6cd: c3 retq
没有条件跳转,位于同一缓存行(虽然我不能百分之百确定 - ELF 加载器确切地将指令放在哪里?这里的 64 字节边界是否映射到内存中的 64 字节边界?)... 这个抖动是从哪里来的?如果我通过 zsh 执行该代码 1000 次(每次重新启动程序),我得到的值从 12 到 46 不等,并有几个中间值。由于我的内核已禁用频率缩放,因此只剩下中断。现在我愿意相信,在 1000 次执行中,会有一次被中断。但我不愿相信有 90% 被中断了(我们正在讨论 ns 级别的时间间隔!那么中断应该来自哪里呢?!)。
所以,我的问题如下:
- 为什么代码不是确定性的,即为什么每次运行都无法得到相同的数字?
- 是否可以推断出运行时间,至少对于这个非常简单的代码片段而言?我能保证运行时间的下限吗(使用工程原理,而不是测量加上希望)?
- 如果不能,那么非确定性行为的源是什么?CPU 的哪个部件(或计算机的其他部分?)在这里扔骰子?