【问题标题】:How to calculate time for an asm delay loop on x86 linux?如何计算 x86 linux 上的 asm 延迟循环的时间?
【发布时间】:2018-09-30 03:15:28
【问题描述】:

我正在通过此链接 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

这个汇编代码会产生多少延迟?

如果我想生成 100nsec 或 1000nsec 或任何其他微秒延迟,我需要在寄存器中加载什么初始值?

我在 Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz 和 Core-i3 CPU 3470 @ 3.20GHz 处理器中使用 ubuntu 16.04(32 位和 64 位)。

提前谢谢你。

【问题讨论】:

  • 延迟不是确定性的,您也不应该期望它是确定性的。
  • @old_timer:为什么你认为缓存、预取、分支预测、线程和内存延迟有什么影响?是否组装;)
  • @Klaus 你知道在这个平台上它实际上是微编码的,所以如果它是用微码编写的,那么它可能是确定性的。只需在指令集中添加延迟指令即可
  • @old_timer:这些都是 OP 的 Kaby Lake 和 IvyBridge 上的单指令。问题不是微码,而是动态 CPU 频率、来自其他超线程的竞争以及中断延迟。甚至可能是内核不知道的系统管理模式中断。 (Linux 不是硬实时操作系统,除此之外,现代 PC 充满了巫术。)在每个核心时钟周期进行 1 次迭代时,循环是完全可预测的,无论其中是否有 nop。 (agner.org/optimize)
  • @PeterCordes 请重新阅读最后两个 cmets 并意识到两者都不严肃,只是有点幽默。我是否也应该在我的评论中添加笑容?此时编辑为时已晚。

标签: linux assembly x86 delay intel


【解决方案1】:

在现代 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,包括使用“jiffies”时钟的ndelay(unsigned long nsecs) - 速度估计至少休眠足够长的时间。IDK 准确度如何,或者当时钟速度较低时它有时会比所需的休眠时间长得多,或者它是否会随着 CPU 频率的变化而更新校准。)


在最近的 Intel/AMD CPU 上,您的(内部)循环完全可以预测为每个核心时钟周期 1 次迭代,无论其中是否有 nop。它低于 4 个融合域微指令,因此您的 CPU 的每时钟 1 个循环吞吐量成为瓶颈。 (请参阅 Agner Fog's x86 microarch guide,或使用 perf stat ./a.out 自己为大迭代计数计时。)除非在同一物理内核上存在来自另一个超线程的竞争...

或者,除非内部循环跨越 32 字节边界,否则在 Skylake 或 Kaby Lake 上(微码更新禁用循环缓冲区以解决设计错误)。然后你的dec / jnz 循环可以每 2 个周期运行 1 个,因为它需要从 2 个不同的 uop-cache 行中获取。

我建议省略 nop,以便在更多 CPU 上也有更好的机会使其成为每时钟 1 个。无论如何您都需要对其进行校准,因此更大的代码占用空间没有帮助(因此也不要进行额外的对齐)。 (如果您需要确保最小延迟时间,请确保在 CPU 处于最大加速时进行校准。)

如果您的内部循环不是那么小(例如更多 nops),请参阅 Is performance reduced when executing loops whose uop count is not a multiple of processor width? 了解当 uop 计数不是 8 的倍数时前端吞吐量的详细信息。SKL / KBL 已禁用即使对于微小的循环,循环缓冲区也会从 uop 缓存中运行。


但 x86 没有固定的时钟频率(和 transitions between frequency states stop the clock for ~20k clock cycles (8.5us),在 Skylake CPU 上)。

如果在启用中断的情况下运行此程序,则中断是另一个不可预测的延迟来源。(即使在内核模式下,Linux 通常也启用中断。一个禁用中断的延迟循环用于数万时钟循环似乎是个坏主意。)

如果在用户空间中运行,那么我希望您使用的是带有实时支持编译的内核。但即便如此,Linux 也不是完全为硬实时操作而设计的,所以我不确定你能做到多好。

系统管理模式中断是另一个延迟来源,即使是内核也不知道。根据英特尔的 PC BIOS 测试套件,2013 年的PERFORMANCE IMPLICATIONS OF SYSTEM MANAGEMENT MODE 表示,对于 SMI 而言,150 微秒被认为是“可接受的”延迟。现代个人电脑充满了巫术。我认为/希望大多数主板上的固件没有太多的 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 等待当前时间到达最后期限。您需要知道 reference 频率,它与核心时钟无关,因此它是固定且不间断的(在现代 CPU 上;有用于不变和不间断 TSC 的 CPUID 功能位。Linux 会检查这一点,所以您可以在 /proc/cpuinfo 中查找 constant_tscnonstop_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 sleeps 则没有问题。

 ;;; 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&lt;&lt;16); 这样的定点数。这意味着我们只在延迟循环内使用整数乘法和移位。 在启动期间使用 C 代码设置 ref_freq_fixedpoint 并为 CPU 设置正确的值

如果为每个目标 CPU 重新编译它,乘法常数可以是 imul 的立即操作数,而不是从内存中加载。

pause 在 Skylake 上休眠约 100 个时钟,但在以前的 Intel uarches 上仅休眠约 5 个时钟。因此,它会稍微影响时序精度,当 CPU 频率降至 ~1GHz 时,可能会在截止日期后休眠 100 ns。或者以正常的 ~3GHz 速度,更接近 +33ns。

持续运行,这个循环将我的 Skylake i7-6700k 的一个核心在 ~3.9GHz 下加热了 ~15 摄氏度,没有 pause,但只有 ~9 摄氏度 pause。 (使用大型 CoolerMaster Gemini II 热管冷却器,基线温度约为 30C,但机箱中的气流较低,以保持风扇噪音低。)

将开始时间测量值调整为比实际时间更早可以让您补偿一些额外的开销,例如离开循环时的分支错误预测,以及首先rdtsc 直到可能接近其执行结束时才对时钟进行采样。乱序执行可以让rdtsc提前运行;您可以使用lfence,或考虑rdtscp,在调用延迟函数之前阻止第一个时钟样本在指令之前发生乱序。

将偏移量保留在变量中也可以让您校准恒定偏移量。如果您可以在启动时自动执行此操作,则可以很好地处理 CPU 之间的变化。但是你需要一些高精度的计时器才能让它工作,这已经基于rdtsc

将第一个 RDTSC 内联到调用者中并将低 32 位作为另一个函数 arg 传递将确保“计时器”立即启动,即使在调用延迟函数时存在指令缓存未命中或其他管道停顿。所以 I$ 错过时间将是延迟间隔的一部分,而不是额外的开销。


rdtsc上旋转的优势:

如果发生任何延迟执行的事情,循环仍会在截止日期前退出,除非在截止日期过去时执行当前被阻塞(在这种情况下,您会被任何方法搞砸)。

因此,您可以使用 CPU 时间,直到当前时间比您第一次检查时晚 n * freq 纳秒,而不是精确使用 n 周期的 CPU 时间。

使用简单的计数器延迟循环,在 4GHz 下足够长的延迟会使您在 0.8GHz 下睡眠时间超过 4 倍(最新 Intel CPU 的典型最低频率)。

这确实会运行rdtsc 两次,因此它不适用于只有几纳秒的延迟。 (rdtsc 本身约为 20 微秒,在 Skylake/Kaby Lake 上每 25 个时钟的吞吐量为 1 个。)我认为对于数百或数千纳秒的忙碌等待来说,这可能是最不坏的解决方案,不过。

缺点:迁移到具有未同步 TSC 的另一个内核可能会导致睡眠时间错误。但除非您的延迟非常长,否则迁移时间会更长比预期的延迟。最坏的情况是在迁移后再次休眠延迟时间。我进行比较的方式:(now - start) &lt; count,而不是寻找某个目标目标计数,这意味着当now-start 是一个很大的数字时,无符号环绕将使比较为真。当柜台环绕时,您不会被困在几乎一秒钟的睡眠中。

缺点:maybe you want to sleep for a certain number of core cycles,或者在 CPU 休眠时暂停计数。

缺点:旧 CPU 可能没有不间断/不变的 TSC。在启动时检查这些 CPUID 功能位,并可能使用备用延迟循环,或者至少在校准时将其考虑在内。另请参阅Get CPU cycle count?,了解我对 RDTSC 行为的规范答案的尝试。


未来的 CPU:在具有 WAITPKG CPUID 功能的 CPU 上使用 tpause

(我不知道未来哪些 CPU 应该会有这个。)

类似于pause,但将逻辑核心置于休眠状态,直到 TSC = 您在 EDX:EAX 中提供的值。所以你可以rdtsc 找出当前时间,add / adc 睡眠时间缩放到 TSC 刻度到 EDX:EAX,然后运行 ​​tpause

有趣的是,它需要另一个输入寄存器,您可以在其中放置 0 以获得更深的睡眠(对另一个超线程更友好,可能会退回到单线程模式),或 1 以获得更快的唤醒和更少的功率-保存。

你不会想用它来睡几秒钟;您想将控制权交还给操作系统。但是,如果它很远,您可以进行操作系统睡眠以接近您的目标唤醒,然后在剩下的任何时间都使用mov ecx,1xor ecx,ecx / tpause ecx

半相关(也是 WAITPKG 扩展的一部分)是更有趣的 umonitor / umwait,它(如特权监视器/mwait)可以在看到内存更改时唤醒内核地址范围。对于超时,它在 TSC = EDX:EAX 上的唤醒与 tpause 相同。

【讨论】:

  • 基于繁忙循环的非常短时间的等待并不算太疯狂,即使在 x86 上也是如此,而且实际上 Linux 内核为此计算了 bogomips 值。是的,你会得到异常值,但 AFAIK 它们都在同一个方向:比你想要的更长的等待,这通常是异常值的类型,它不会破坏等待的根本原因:通常你想睡在在做某事之前至少 T,比如检查硬件响应,但更长的时间是可以的(但如果它发生得太频繁,则不可取)。
  • 也许更相关的是,如果这些“更长”的异常值是不可接受的,那么无论如何你都会被搞砸,因为你无法真正避免它们:导致异常值的事情往往会完全暂停 CPU用户的 PoV,所以充其量你可以检测到它们,但不能阻止它们。因此,对于按相关事件顺序的睡眠,繁忙循环似乎与任何其他方法一样好。对于更长的睡眠时间,像投票 rdtsc 这样的事情开始有更好的 QoI,因为您可以取消延迟并更接近您的截止日期。
  • 您可能无法使用 rdtsc 的两个原因:如果它在不同 CPU 上的同步不够紧密(尽管上下文切换可能会影响您的时间),您可能会这样做像睡眠太短这样的坏事,或者如果你真的想确保你的延迟是在 CPU 周期而不是挂钟时间和/或你真的想在 CPU 停止时停止计数。
  • 哦,关于带有未同步 TSC 的上下文切换和其他用例的要点。
  • 是的,我同意。如果你真的想要精确,你总是可以在主循环中使用pause; rdtsc,但是当你计算出在截止日期之前你只剩下不到一个pause; rdtsc迭代时,剩下的时间进入一个非常小的延迟循环。如果延迟循环很短,大部分问题都可以消除。
猜你喜欢
  • 2011-12-08
  • 2021-04-19
  • 1970-01-01
  • 2012-08-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多