我在装配中经历了这个链接延迟,以增加装配延迟.我想通过添加不同的延迟值来执行一些实验.
生成延迟的有用代码
; start delay
mov bp, 43690
mov si, 43690
delay2:
dec bp
nop
jnz delay2
dec si
cmp si,0
jnz delay2
; end delay
我从代码中理解的是,延迟与执行nop指令所花费的时间成比例(43690x43690).所以在不同系统和不同版本的操作系统中,延迟会有所不同.我对吗?
任何人都可以向我解释如何计算nsec的延迟量,下面的汇编代码正在生成,以便我可以结束我在实验设置中添加的延迟的实验?
这是我用来生成延迟而不理解使用43690值的逻辑的代码(我在原始源代码中只对一个循环使用了一个循环).为了产生不同的延迟(不知道它的值),我只改变了数字43690到403690或其他值.
32位操作系统中的代码
movl $43690, %esi ; ---> if I vary this 4003690 then delay value ??
.delay2:
dec %esi
nop
jnz .delay2
这个汇编代码会产生多少延迟?
如果我想在microsec中生成100nsec或1000nsec或任何其他延迟,那么我需要在寄存器中加载什么初始值?
我使用的是ubuntu 16.04(32位和64位),Intel(R)Core(TM)i5-7200U CPU @ 2.50GHz和Core-i3 CPU 3470 @ 3.20GHz处理器.
先感谢您.
1> Peter Cordes..:
从现代x86 PC上的延迟循环的固定计数中获取准确且可预测的时序没有很好的方法,特别是在非实时操作系统(如Linux)下的用户空间中. (但是你可以在rdtsc
非常短的时间内继续前进;见下文).你可以使用一个简单的延迟循环,如果你需要睡眠至少足够长,当出现问题时可以睡得更久.
通常你想睡觉并让操作系统唤醒你的进程,但这对Linux上的延迟只有几微秒是行不通的. nanosleep
可以表达它,但内核没有按照这样精确的时间安排.请参阅如何使线程休眠/阻塞纳秒(或至少毫秒)?.在启用了Meltdown + Spectre缓解的内核上,无论如何,内核的往返时间都要超过一微秒.
(或者你是在内核中做这个吗?我认为Linux已经有一个校准的延迟循环.无论如何,它有一个标准的延迟API:https://www.kernel.org/doc/Documentation/timers/timers- howto.txt,包括ndelay(unsigned long nsecs)
使用"jiffies"时钟速度估计至少足够长时间睡眠.IDK是多么准确,或者如果有时睡眠时间比时钟速度低,或者如果它更新校准随着CPU频率的变化.)
在最近的Intel/AMD CPU上,您的(内部)循环在每个核心时钟周期的1次迭代中是完全可预测的,无论其中是否存在nop
.它属于4个融合域uop,因此您在CPU的每时钟1个循环吞吐量上遇到瓶颈.(参见Agner Fog的x86微指南指南,或者自己计算大量迭代次数perf stat ./a.out
.) 除非在同一物理核心上存在来自另一个超线程的竞争 ......
或者除非内部循环跨越32字节边界,在Skylake或Kaby Lake上(循环缓冲区由微代码更新禁用以解决设计错误).然后你的dec / jnz
循环可以每2个循环运行一次,因为它需要从2个不同的uop-cache行获取.
我建议不要让nop
更多的CPU在每个时钟上更好的机会.无论如何,您需要校准它,因此更大的代码占用空间是没有用的(因此也要省略额外的对齐).(如果您需要确保最小延迟时间,请确保在CPU处于最大涡轮增压时进行校准.)
如果您的内部循环不是那么小(例如,更多nop
s),请参阅执行uop计数不是处理器宽度倍数的循环时性能是否降低?有关uop计数不是8的倍数的前端吞吐量的详细信息.带有禁用循环缓冲区的SKL/KBL从uop缓存运行,即使是微循环也是如此.
但是x86没有固定的时钟频率(在Skylake CPU上,频率状态之间的转换会使时钟停止约20k个时钟周期(8.5us)).
如果在启用中断的情况下运行此操作,则中断是另一个不可预测的延迟源. (即使在内核模式下,Linux通常也会启用中断.数万个时钟周期的中断禁用延迟循环似乎是一个坏主意.)
如果在用户空间中运行,那么我希望您使用的是使用实时支持编译的内核.但即便如此,Linux还没有完全针对硬实时操作而设计,所以我不确定你能获得多少好处.
系统管理模式中断是内核不知道的另一个延迟源. 系统管理模式的性能影响从2013年开始表示,根据英特尔的PC BIOS测试套件,150微秒被认为是SMI的"可接受"延迟.现代电脑充满伏都教.我认为/希望大多数主板上的固件没有太多的SMM开销,并且SMI在正常操作中非常罕见,但我不确定.另请参阅评估Linux-CentOS/Intel计算机上的SMI(系统管理中断)延迟
极低功耗的Skylake CPU以一些占空比停止其时钟,而不是降低时钟并持续运行.请参阅此内容,以及英特尔关于Skylake电源管理的IDF2015演示文稿.
旋转RDTSC
直到正确的挂钟时间
如果你真的需要忙碌等待,请rdtsc
等待当前时间到达截止日期.您需要知道参考频率,它与核心时钟无关,因此它是固定的和不间断的(在现代CPU上;对于不变和不间断的TSC,有CPUID功能位.Linux检查这个,所以你可以查看/ proc/cpuinfo for constant_tsc
和nonstop_tsc
,但实际上你应该在程序启动时自己检查CPUID并计算出RDTSC频率(某种方式......)).
我写了这样一个循环,作为一个愚蠢的计算机技巧练习的一部分:一个秒表在x86机器代码的最少字节.大多数代码大小用于字符串操作以增加00:00:00
显示并打印它.我为我的CPU硬编码了4GHz RDTSC频率.
对于小于2 ^ 32个参考时钟的睡眠,您只需要查看计数器的低32位.如果你正确地进行比较,环绕会照顾好自己.对于1秒秒表,4.3GHz的CPU会出现问题,但是对于nsec/usec睡眠,没有问题.
;;; Untested, NASM syntax
default rel
section .data
; RDTSC frequency in counts per 2^16 nanoseconds
; 3200000000 would be for a 3.2GHz CPU like your i3-3470
ref_freq_fixedpoint: dd 3200000000 * (1<<16) / 1000000000
; The actual integer value is 0x033333
; which represents a fixed-point value of 3.1999969482421875 GHz
; use a different shift count if you like to get more fractional bits.
; I don't think you need 64-bit operand-size
; nanodelay(unsigned nanos /*edi*/)
; x86-64 System-V calling convention
; clobbers EAX, ECX, EDX, and EDI
global nanodelay
nanodelay:
; take the initial clock sample as early as possible.
; ideally even inline rdtsc into the caller so we don't wait for I$ miss.
rdtsc ; edx:eax = current timestamp
mov ecx, eax ; ecx = start
; lea ecx, [rax-30] ; optionally bias the start time to account for overhead. Maybe make this a variable stored with the frequency.
; then calculate edi = ref counts = nsec * ref_freq
imul edi, [ref_freq_fixedpoint] ; counts * 2^16
shr edi, 16 ; actual counts, rounding down
.spinwait: ; do{
pause ; optional but recommended.
rdtsc ; edx:eax = reference cycles since boot
sub eax, ecx ; delta = now - start. This may wrap, but the result is always a correct unsigned 0..n
cmp eax, edi ; } while(delta
为了避免频率计算的浮点数,我使用了定点uint32_t ref_freq_fixedpoint = 3.2 * (1<<16);
.这意味着我们只需在延迟循环内使用整数乘法和移位. 使用C代码ref_freq_fixedpoint
在启动期间使用正确的CPU值进行设置.
如果为每个目标CPU重新编译它,则乘法常量可以是立即操作数,imul
而不是从内存加载.
pause
在Skylake上睡觉约100个时钟,但在以前的英特尔搜索中只能用于约5个时钟.因此它会稍微损害定时精度,当CPU频率降至~1GHz时,可能会在截止时间之前休眠100 ns.或者以正常~3GHz的速度,更像是高达+ 33ns.
运行汽车无,这个循环加热我SKYLAKE微架构i7-6700k的一个核心在〜3.9GHz由〜15°C,无pause
,但只能通过〜9下用pause
.(使用大型CoolerMaster Gemini II热管冷却器,从~30C的基线开始,但在这种情况下气流低,以保持风扇噪音低.)
将开始时间测量调整为比实际更早,可以让您补偿一些额外的开销,例如离开环路时的分支错误预测,以及第一个rdtsc
不接受时钟采样的事实,直到可能接近执行结束.乱序执行可以让rdtsc
早点运行; 您可以使用lfence
或考虑rdtscp
在调用延迟函数之前停止第一个时钟样本在指令之前发生乱序.
将偏移保持在变量中也可以校准常数偏移.如果您可以在启动时自动执行此操作,那么处理CPU之间的差异可能会很好.但是你需要一些高精度计时器才能工作,这已经基于rdtsc
.
将第一个内联RDTSC
到调用者并将低32位作为另一个函数arg传递将确保"定时器"立即启动,即使在调用延迟函数时存在指令缓存未命中或其他管道停顿.所以I $ miss time将是延迟间隔的一部分,而不是额外的开销.
旋转的优点rdtsc
:
如果发生延迟执行的任何事情,循环仍然会在截止日期之前退出,除非在截止日期过后执行被阻止(在这种情况下,你被任何方法搞砸了).
因此,不使用精确n
的CPU时间周期,而是使用CPU时间,直到当前时间n * freq
比第一次检查时晚几毫秒.
使用简单的计数器延迟环路,在4GHz时足够长的延迟会使您在0.8GHz(在最近的Intel CPU上的典型最低频率)下睡眠时间超过4倍.
这确实运行了rdtsc
两次,因此它不适合仅几纳秒的延迟.(rdtsc
本身是~20 uops,并且在Skylake/Kaby Lake上每25个时钟的吞吐量为一个.) 我认为这可能是繁忙等待数百或数千纳秒的最不好的解决方案.
缺点:迁移到另一个未通过TSC的核心可能导致在错误的时间内睡眠.但除非您的延迟很长,否则迁移时间将超过预期的延迟.最糟糕的情况是在迁移后再次延迟休眠时间.我进行比较的方式:(now - start) ,而不是寻找某个目标目标计数,意味着无符号环绕将使得当now-start
大数字时比较为真.当计数器缠绕时,你不会被困住几乎整整一秒钟.
缺点:您可能想要睡眠一定数量的核心周期,或者在CPU处于睡眠状态时暂停计数.
缺点:旧CPU可能没有不间断/不变的TSC.在启动时检查这些CPUID功能位,并可能使用备用延迟循环,或至少在校准时将其考虑在内.另请参阅获取CPU周期数?因为我尝试了关于RDTSC行为的规范答案.
未来的CPU:tpause
在具有WAITPKG CPUID功能的CPU上使用.
(我不知道将来会有哪些CPU.)
就像这样pause
,但是让逻辑核心睡眠,直到TSC =您在EDX中提供的值:EAX.所以你可以rdtsc
找出当前时间,add / adc
将睡眠时间缩放到TSC滴答到EDX:EAX,然后运行tpause
.
有趣的是,它需要另一个输入寄存器,您可以在其中0
进行更深入的睡眠(对其他超线程更友好,可能会回退到单线程模式),或者1
更快的唤醒和更少的省电.
你不想用它来睡几秒钟; 你想把控制权交还给操作系统.但是你可以做一个操作系统睡眠来接近你的目标唤醒,如果它很远,那么mov ecx,1
或xor ecx,ecx
/ tpause ecx
无论什么时候离开.
半相关(也是WAITPKG扩展的一部分)是更有趣的umonitor
/ umwait
,它(如特权监视器/ mwait)可以在看到地址范围内的内存更改时唤醒核心.对于超时,它在TSC = EDX:EAX as上具有相同的唤醒tpause
.