NASM 中的 RDTSCP 总是 returns 相同的值(对单个指令计时)
RDTSCP in NASM always returns the same value (timing a single instruction)
我在 NASM 中使用 RDTSC 和 RDTSCP 来测量各种汇编语言指令的机器周期以帮助优化。
我阅读了英特尔的 Gabriele Paoloni 的 "How to Benchmark Code Execution Times on Intel IA-32 and IA-64 Instruction Set Architectures"(2010 年 9 月)和其他网络资源(其中大部分是 C 语言的示例)。
使用下面的代码(从 C 翻译而来),我测试了各种指令,但 RDTSCP 总是 return 在 RDX 中为零,在 RAX 中为 7。我一开始以为7是循环数,但显然不是所有的指令都需要7个循环。
rdtsc
cpuid
addsd xmm14,xmm1 ; Instruction to time
rdtscp
cpuid
这个 returns 7,这并不奇怪,因为在某些架构上 adddd 是 7 个周期,包括延迟。前两条指令可以(根据某些人的说法)颠倒过来,首先是 cpuid,然后是 rdtsc,但这在这里没有区别。
当我将指令更改为 2 周期指令时:
rdtsc
cpuid
add rcx,rdx ; Instruction to time
rdtscp
cpuid
这也 return在 rax 中为 7,在 rdx 中为 0。
所以我的问题是:
如何访问和解释 return 在 RDX:RAX 中编辑的值?
为什么 RDX 总是 return 零,它应该是什么 return?
更新:
如果我将代码更改为:
cpuid
rdtsc
mov [start_time],rax
addsd xmm14,xmm1 ; INSTRUCTION
rdtscp
mov [end_time],rax
cpuid
mov rax,[end_time]
mov rdx,[start_time]
sub rax,rdx
我在 rax 中得到 64,但这听起来循环太多了。
您的第一个代码(导致标题问题)有错误,因为它用 EAX,EBX 中的 cpuid
结果覆盖了 rdtsc
和 rdtscp
结果,ECX 和 EDX.
使用lfence
代替cpuid
;在 Intel 上永远和启用了 Spectre 缓解的 AMD 上,lfence
将序列化指令流,从而用 rdtsc
.
做你想做的事
请记住,RDTSC 计算的是参考周期,而不是核心时钟周期。 Get CPU cycle count? 以及关于 RDTSC 的更多信息。
您的测量区间内没有 cpuid
或 lfence
。但是你 do 在测量间隔中有 rdtscp
本身。 Back-to-back rdtscp
并不快,如果您 运行 没有预热 CPU,64 个参考周期听起来完全合理。空闲时钟速度通常比参考周期慢很多; 1 个参考周期等于或接近 "sticker" 频率,例如Intel CPUs 上的最大 non-turbo 持续频率。例如“4GHz”Skylake 上的 4008 MHz CPU.
这不是您为单个指令计时的方式
重要的是另一条指令可以使用结果之前的延迟,而不是它完全退出 out-of-order back-end 之前的延迟。 RDTSC 可能很有用对于计时 相对变化 一次加载或一条存储指令需要多长时间,但开销意味着您不会获得好的绝对时间。
不过,您可以尝试减去测量开销。例如clflush to invalidate cache line via C function. And see also the followups: Using time stamp counter and clock_gettime for cache miss and Memory latency measurement with time stamp counter.
这是我通常用来分析短块指令的延迟或吞吐量(以及 uops 融合和未融合域)的东西。像这里一样调整你如何使用它来限制延迟瓶颈,如果你只想测试吞吐量,则不要调整它。例如使用具有足够不同寄存器的 %rep
块来隐藏延迟,或者在短块之后使用 pxor xmm3, xmm3
打破依赖链,让 out-of-order exec 发挥其魔力。 (只要你不在 front-end 上遇到瓶颈。)
您可能想使用 NASM 的 smartalign 包,或使用 YASM,以避免 ALIGN 指令的 single-byte NOP 指令墙。 NASM 默认为非常愚蠢的 NOP,即使在始终支持 long-NOP 的 64 位模式下也是如此。
global _start
_start:
mov ecx, 1000000000
; linux static executables start with XMM0..15 already zeroed
align 32 ; just for good measure to avoid uop-cache effects
.loop:
;; LOOP BODY, put whatever you want to time in here
times 4 addsd xmm4, xmm3
dec ecx
jnz .loop
mov eax, 231
xor edi, edi
syscall ; x86-64 Linux sys_exit_group(0)
运行 这与类似这样的东西 one-liner 将其链接到静态可执行文件并使用 perf stat
、 配置文件,您可以 up-arrow和 re-run 每次更改源时:
(我实际上将 nasm+ld + 可选反汇编成一个名为 asm-link
的 shell 脚本,以便在我不进行分析时节省输入。反汇编确保什么是在你的循环中是你 想要 进行分析的,特别是如果你的代码中有一些 %if
东西。而且如果你想要的话,它就在你的终端上,就在分析之前在脑海中测试理论时向后滚动。)
t=testloop; nasm -felf64 -g "$t.asm" && ld "$t.o" -o "$t" && objdump -drwC -Mintel "$t" &&
taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread -r4 ./"$t"
i7-6700k 在 3.9GHz 下的结果(当前 perf
有一个 unit-scaling 显示辅助列的错误。它已在上游修复,但 Arch Linux还没有更新。):
Performance counter stats for './testloop' (4 runs):
4,106.09 msec task-clock # 1.000 CPUs utilized ( +- 0.01% )
17 context-switches # 4.080 M/sec ( +- 5.65% )
0 cpu-migrations # 0.000 K/sec
2 page-faults # 0.487 M/sec
16,012,778,144 cycles # 3900323.504 GHz ( +- 0.01% )
1,001,537,894 branches # 243950284.862 M/sec ( +- 0.00% )
6,008,071,198 instructions # 0.38 insn per cycle ( +- 0.00% )
5,013,366,769 uops_issued.any # 1221134275.667 M/sec ( +- 0.01% )
5,013,217,655 uops_executed.thread # 1221097955.182 M/sec ( +- 0.01% )
4.106283 +- 0.000536 seconds time elapsed ( +- 0.01% )
在我的 i7-6700k (Skylake) 上,addsd
有 4 个周期的延迟,0.5c 的吞吐量。 (即每个时钟 2 个,如果延迟不是瓶颈的话)。参见 https://agner.org/optimize/, https://uops.info/, and http://instlatx64.atw.hu/。
每个 b运行ch = 16 个周期每个链 4 addsd
= addsd
的 4 个周期延迟,再现 Agner Fog 的 4 个周期测量即使对于包含少量启动开销和中断开销的测试,也优于 100 分之一。
选择不同的计数器进行记录。添加 :u
,比如 instructions:u
到 perf even 将只计算 user-space 指令,不包括中断处理程序期间的任何 运行。我通常不这样做,所以我可以将这种开销视为 wall-clock 时间解释的一部分。但如果这样做,cycles:u
可以非常匹配 与 instructions:u
。
-r4
运行它 4 次并取平均值,这有助于查看是否有很多 run-to-run 变化,而不是仅仅从 ECX 中的较高值获得一个平均值。
调整您的初始 ECX 值,使总时间大约为 0.1 到 1 秒,这通常足够了,特别是如果您的 CPU 非常快地达到最大涡轮增压(例如带有硬件 P-states 的 Skylake和相当激进的 energy_performance_preference)。或最大 non-turbo 禁用 turbo。
但这计算的是核心时钟周期,而不是参考周期,因此无论 CPU 频率如何变化,它仍然会给出相同的结果。 (+- 在 t运行 期间停止时钟的一些噪音。)
我在 NASM 中使用 RDTSC 和 RDTSCP 来测量各种汇编语言指令的机器周期以帮助优化。
我阅读了英特尔的 Gabriele Paoloni 的 "How to Benchmark Code Execution Times on Intel IA-32 and IA-64 Instruction Set Architectures"(2010 年 9 月)和其他网络资源(其中大部分是 C 语言的示例)。
使用下面的代码(从 C 翻译而来),我测试了各种指令,但 RDTSCP 总是 return 在 RDX 中为零,在 RAX 中为 7。我一开始以为7是循环数,但显然不是所有的指令都需要7个循环。
rdtsc
cpuid
addsd xmm14,xmm1 ; Instruction to time
rdtscp
cpuid
这个 returns 7,这并不奇怪,因为在某些架构上 adddd 是 7 个周期,包括延迟。前两条指令可以(根据某些人的说法)颠倒过来,首先是 cpuid,然后是 rdtsc,但这在这里没有区别。
当我将指令更改为 2 周期指令时:
rdtsc
cpuid
add rcx,rdx ; Instruction to time
rdtscp
cpuid
这也 return在 rax 中为 7,在 rdx 中为 0。
所以我的问题是:
如何访问和解释 return 在 RDX:RAX 中编辑的值?
为什么 RDX 总是 return 零,它应该是什么 return?
更新:
如果我将代码更改为:
cpuid
rdtsc
mov [start_time],rax
addsd xmm14,xmm1 ; INSTRUCTION
rdtscp
mov [end_time],rax
cpuid
mov rax,[end_time]
mov rdx,[start_time]
sub rax,rdx
我在 rax 中得到 64,但这听起来循环太多了。
您的第一个代码(导致标题问题)有错误,因为它用 EAX,EBX 中的 cpuid
结果覆盖了 rdtsc
和 rdtscp
结果,ECX 和 EDX.
使用lfence
代替cpuid
;在 Intel 上永远和启用了 Spectre 缓解的 AMD 上,lfence
将序列化指令流,从而用 rdtsc
.
请记住,RDTSC 计算的是参考周期,而不是核心时钟周期。 Get CPU cycle count? 以及关于 RDTSC 的更多信息。
您的测量区间内没有 cpuid
或 lfence
。但是你 do 在测量间隔中有 rdtscp
本身。 Back-to-back rdtscp
并不快,如果您 运行 没有预热 CPU,64 个参考周期听起来完全合理。空闲时钟速度通常比参考周期慢很多; 1 个参考周期等于或接近 "sticker" 频率,例如Intel CPUs 上的最大 non-turbo 持续频率。例如“4GHz”Skylake 上的 4008 MHz CPU.
这不是您为单个指令计时的方式
重要的是另一条指令可以使用结果之前的延迟,而不是它完全退出 out-of-order back-end 之前的延迟。 RDTSC 可能很有用对于计时 相对变化 一次加载或一条存储指令需要多长时间,但开销意味着您不会获得好的绝对时间。
不过,您可以尝试减去测量开销。例如clflush to invalidate cache line via C function. And see also the followups: Using time stamp counter and clock_gettime for cache miss and Memory latency measurement with time stamp counter.
这是我通常用来分析短块指令的延迟或吞吐量(以及 uops 融合和未融合域)的东西。像这里一样调整你如何使用它来限制延迟瓶颈,如果你只想测试吞吐量,则不要调整它。例如使用具有足够不同寄存器的 %rep
块来隐藏延迟,或者在短块之后使用 pxor xmm3, xmm3
打破依赖链,让 out-of-order exec 发挥其魔力。 (只要你不在 front-end 上遇到瓶颈。)
您可能想使用 NASM 的 smartalign 包,或使用 YASM,以避免 ALIGN 指令的 single-byte NOP 指令墙。 NASM 默认为非常愚蠢的 NOP,即使在始终支持 long-NOP 的 64 位模式下也是如此。
global _start
_start:
mov ecx, 1000000000
; linux static executables start with XMM0..15 already zeroed
align 32 ; just for good measure to avoid uop-cache effects
.loop:
;; LOOP BODY, put whatever you want to time in here
times 4 addsd xmm4, xmm3
dec ecx
jnz .loop
mov eax, 231
xor edi, edi
syscall ; x86-64 Linux sys_exit_group(0)
运行 这与类似这样的东西 one-liner 将其链接到静态可执行文件并使用 perf stat
、 配置文件,您可以 up-arrow和 re-run 每次更改源时:
(我实际上将 nasm+ld + 可选反汇编成一个名为 asm-link
的 shell 脚本,以便在我不进行分析时节省输入。反汇编确保什么是在你的循环中是你 想要 进行分析的,特别是如果你的代码中有一些 %if
东西。而且如果你想要的话,它就在你的终端上,就在分析之前在脑海中测试理论时向后滚动。)
t=testloop; nasm -felf64 -g "$t.asm" && ld "$t.o" -o "$t" && objdump -drwC -Mintel "$t" &&
taskset -c 3 perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread -r4 ./"$t"
i7-6700k 在 3.9GHz 下的结果(当前 perf
有一个 unit-scaling 显示辅助列的错误。它已在上游修复,但 Arch Linux还没有更新。):
Performance counter stats for './testloop' (4 runs):
4,106.09 msec task-clock # 1.000 CPUs utilized ( +- 0.01% )
17 context-switches # 4.080 M/sec ( +- 5.65% )
0 cpu-migrations # 0.000 K/sec
2 page-faults # 0.487 M/sec
16,012,778,144 cycles # 3900323.504 GHz ( +- 0.01% )
1,001,537,894 branches # 243950284.862 M/sec ( +- 0.00% )
6,008,071,198 instructions # 0.38 insn per cycle ( +- 0.00% )
5,013,366,769 uops_issued.any # 1221134275.667 M/sec ( +- 0.01% )
5,013,217,655 uops_executed.thread # 1221097955.182 M/sec ( +- 0.01% )
4.106283 +- 0.000536 seconds time elapsed ( +- 0.01% )
在我的 i7-6700k (Skylake) 上,addsd
有 4 个周期的延迟,0.5c 的吞吐量。 (即每个时钟 2 个,如果延迟不是瓶颈的话)。参见 https://agner.org/optimize/, https://uops.info/, and http://instlatx64.atw.hu/。
每个 b运行ch = 16 个周期每个链 4 addsd
= addsd
的 4 个周期延迟,再现 Agner Fog 的 4 个周期测量即使对于包含少量启动开销和中断开销的测试,也优于 100 分之一。
选择不同的计数器进行记录。添加 :u
,比如 instructions:u
到 perf even 将只计算 user-space 指令,不包括中断处理程序期间的任何 运行。我通常不这样做,所以我可以将这种开销视为 wall-clock 时间解释的一部分。但如果这样做,cycles:u
可以非常匹配 与 instructions:u
。
-r4
运行它 4 次并取平均值,这有助于查看是否有很多 run-to-run 变化,而不是仅仅从 ECX 中的较高值获得一个平均值。
调整您的初始 ECX 值,使总时间大约为 0.1 到 1 秒,这通常足够了,特别是如果您的 CPU 非常快地达到最大涡轮增压(例如带有硬件 P-states 的 Skylake和相当激进的 energy_performance_preference)。或最大 non-turbo 禁用 turbo。
但这计算的是核心时钟周期,而不是参考周期,因此无论 CPU 频率如何变化,它仍然会给出相同的结果。 (+- 在 t运行 期间停止时钟的一些噪音。)