我正在浏览此链接delay in assembly以增加装配延迟。我想通过添加不同的延迟值来执行一些实验。
生成延迟的有用代码
; 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处理器。
提前谢谢。
答案 0 :(得分:6)
从现代x86 PC上的延迟循环的固定计数中获取准确且可预测的时序没有很好的方法,尤其是在非实时操作系统(如Linux)下的用户空间。(但是你可以在rdtsc
上进行非常短的延迟;见下文)。你可以使用一个简单的延迟循环,如果你需要至少 ,那么当出现问题时你可以睡得更久。
通常你想要睡觉并让操作系统唤醒你的过程,但这并不适用于Linux上只有几微秒的延迟。 nanosleep
可以表达它,但内核并没有按照这样精确的时间安排。见How to make a thread sleep/block for nanoseconds (or at least milliseconds)?。在启用了Meltdown + Spectre缓解的内核上,内核的往返行程需要的时间超过一微秒。
(或者你在内核中这样做了吗?我认为Linux已经有一个经过校准的延迟循环。无论如何,它有一个标准的延迟API:https://www.kernel.org/doc/Documentation/timers/timers-howto.txt,包括使用ndelay(unsigned long nsecs)
的{{1}} " jiffies"时钟速度估计至少要足够长时间睡眠.IDK有多准确,或者当时钟速度低时有时睡眠时间比需要的时间长,或者如果它将校准更新为CPU频率变化。)
您的(内部)循环在最近的Intel / AMD CPU 上每核心时钟周期1次迭代时完全可预测,无论其中是否有nop
。它属于4个融合域uop,因此您可以锁定CPU的每时钟1个循环吞吐量。 (请参阅Agner Fog's x86 microarch guide,或使用perf stat ./a.out
计算大量迭代计数。)除非来自同一物理核心的另一个超线程的竞争 ... < / p>
除非内部循环跨越32字节边界,否则在Skylake或Kaby Lake上(微码更新禁用循环缓冲区以解决设计错误)。那么你的dec / jnz
循环可以每2个循环运行一次,因为它需要从2个不同的uop-cache行中获取。
我建议省略nop
,以便更多机会在更多CPU上每个时钟1。无论如何,您需要校准它,因此更大的代码占用空间是没有用的(因此也要省略额外的对齐)。 (确保在CPU达到最大涡轮增压时进行校准,如果您需要确保最短延迟时间。)
如果您的内部循环不是那么小(例如nop
s更多),请参阅Is performance reduced when executing loops whose uop count is not a multiple of processor width?,了解当uop计数不是多个时的前端吞吐量的详细信息8.带有禁用循环缓冲区的SKL / KBL从uop缓存运行,即使是微小循环也是如此。
但是,在Skylake CPU上,x86没有固定的时钟频率(和transitions between frequency states stop the clock for ~20k clock cycles (8.5us))。
如果在启用中断的情况下运行此命令,则中断是另一个不可预测的延迟源。(即使在内核模式下,Linux通常也会启用中断。中断禁用延迟循环数万个时钟周期似乎是一个坏主意。)
如果在用户空间中运行,那么我希望您使用通过实时支持编译的内核。但即便如此,Linux还没有完全针对硬实时操作而设计,所以我不确定你能获得多么好的东西。
系统管理模式中断是内核不知道的另一个延迟源。来自2013年的PERFORMANCE IMPLICATIONS OF SYSTEM MANAGEMENT MODE表示150微秒被认为是可接受的&#34;根据英特尔的PC BIOS测试套件,SMI的延迟。现代电脑充满伏都教。我认为/希望大多数主板上的固件没有太多的SMM开销,并且SMI在正常操作中非常罕见,但我不确定。另请参阅Evaluating SMI (System Management Interrupt) latency on Linux-CentOS/Intel machine
极低功耗的Skylake CPU以一些占空比停止其时钟,而不是降低时钟并持续运行。请参阅this以及Intel's IDF2015 presentation about Skylake power management。
RDTSC
直到正确的挂钟时间 如果您真的需要忙碌等待,请转动rdtsc
,等待当前时间到达截止日期。您需要知道引用频率,它与核心时钟无关,因此它是固定的和不间断的(在现代CPU上;对于不变和不间断的TSC,有CPUID功能位.Linux检查这个,所以你可以在/ proc / cpuinfo中查找constant_tsc
和nonstop_tsc
,但实际上你应该在程序启动时自己检查CPUID并计算出RDTSC频率(不知怎的......))。
我写了这样一个循环作为愚蠢的计算机技巧练习的一部分:a stopwatch in the fewest bytes of x86 machine code。大多数代码大小用于字符串操作以递增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 < sleep_counts)
jb .spinwait
ret
为了避免频率计算的浮点数,我使用像uint32_t ref_freq_fixedpoint = 3.2 * (1<<16);
这样的定点。这意味着我们只需在延迟循环内使用整数乘法和移位。 使用C代码在启动期间使用正确的CPU值设置ref_freq_fixedpoint
。
如果为每个目标CPU重新编译它,则乘法常量可以是imul
的立即操作数,而不是从内存加载。
pause
在Skylake上睡了大约100个时钟,但在之前的英特尔搜索中仅用了约5个时钟。因此它会稍微损害定时精度,当CPU频率降至~1GHz时,可能会在截止时间之前休眠100 ns。或者以正常~3GHz的速度,更像是高达+ 33ns。
持续运行,这个循环将我的Skylake i7-6700k的一个核心在~3.9GHz加热~15摄氏度而没有pause
,但只有〜{9 C pause
。 (使用大型CoolerMaster Gemini II热管冷却器从~30C的基线开始,但在这种情况下气流低,以保持风扇噪音低。)
将开始时间测量设置为比实际更早可以让您补偿一些额外的开销,例如离开循环时的分支错误预测,以及首先rdtsc
不会对时钟进行采样,直到可能接近执行结束。乱序执行可以让rdtsc
提前运行;您可以使用lfence
或考虑rdtscp
来阻止第一个时钟样本在调用延迟函数之前在指令之前发生无序。
将偏移保持在变量中也可以校准常数偏移。如果您可以在启动时自动执行此操作,那么处理CPU之间的差异可能会很好。但是你需要一些高精度计时器才能工作,这已经基于rdtsc
。
将第一个RDTSC
内联到调用者并将低32位作为另一个函数arg传递将确保&#34;计时器&#34;即使在调用延迟函数时存在指令缓存未命中或其他流水线停顿,也会立即启动。所以I $ miss time将是延迟间隔的一部分,而不是额外的开销。
rdtsc
上旋转的优势:如果发生延迟执行的任何事情,那么循环仍会在截止日期之前退出,除非在截止日期过后当前执行被阻止(在这种情况下,您已经用任何方法搞砸了)。
因此,不使用精确的n
个CPU时间周期,而是使用CPU时间,直到当前时间比您第一次检查时晚n * freq
纳秒为止。
使用简单的计数器延迟回路,在4GHz时长达足够长的延迟会使您在0.8GHz (最近的Intel CPU上的典型最低频率)下睡眠时间超过4倍。
这会运行rdtsc
两次,因此它不适合仅几纳秒的延迟。 (rdtsc
本身约为20 uops,在Skylake / Kaby Lake上每25个时钟的吞吐量为1个。)我认为这可能是数百或数千个繁忙等待的最不好的解决方案。但是,纳秒。
缺点:迁移到另一个未同步的TSC核心可能导致在错误的时间内睡眠。但除非您的延迟非常长,否则迁移时间会更长比预期的延迟。最糟糕的情况是在迁移后再次延迟休眠时间。我进行比较的方式:(now - start) < count
,而不是寻找某个目标目标计数,意味着当now-start
是一个大数字时,无符号环绕将使比较成立。
下行:maybe you want to sleep for a certain number of core cycles,或在CPU处于睡眠状态时暂停计数。
下行:旧CPU可能没有不间断/不变的TSC。在启动时检查这些CPUID功能位,并可能使用备用延迟循环,或至少在校准时将其考虑在内。另请参阅Get CPU cycle count?我尝试获得有关RDTSC行为的规范答案。
tpause
。(我不知道未来哪些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为tpause
。