为什么 cmp 指令花费太多时间?
Why does cmp instruction cost too much time?
我正在尝试使用 libunwind(使用 linux perf)进行分析,perf top
监控目标进程,我得到了这个组装时间成本屏幕:
0.19 │ mov %rcx,0x18(%rsp) ▒
│ trace_lookup(): ▒
1.54 │ mov 0x8(%r9),%rcx ▒
│ _ULx86_64_tdep_trace(): ▒
0.52 │ and [=10=]x1,%edx ◆
0.57 │ mov %r14d,0xc(%rsp) ▒
0.40 │ mov 0x78(%rsp),%r10 ▒
1.24 │ sub %rdx,%r15 ▒
│ trace_lookup(): ▒
0.35 │ shl %cl,%r12d ▒
│ _ULx86_64_tdep_trace(): ▒
2.18 │ mov 0x90(%rsp),%r8 ▒
│ trace_lookup(): ▒
0.46 │ imul %r15,%r13 ▒
│ _ULx86_64_tdep_trace(): ▒
0.59 │ mov %r15,0x88(%rsp) ▒
│ trace_lookup(): ▒
0.50 │ lea -0x1(%r12),%rdx ▒
1.22 │ shr [=10=]x2b,%r13 ▒
0.37 │ and %r13,%rdx ▒
0.57 │177: mov %rdx,%rbp ▒
0.43 │ shl [=10=]x4,%rbp ▒
1.33 │ add %rdi,%rbp ▒
0.49 │ mov 0x0(%rbp),%rsi ▒
24.40 │ cmp %rsi,%r15 ▒
│ ↓ jne 420 ▒
│ _ULx86_64_tdep_trace(): ▒
2.10 │18e: movzbl 0x8(%rbp),%edx ▒
3.68 │ test [=10=]x8,%dl ▒
│ ↓ jne 370 ▒
1.27 │ mov %edx,%eax ▒
0.06 │ shl [=10=]x5,%eax ▒
0.73 │ sar [=10=]x5,%al ▒
1.70 │ cmp [=10=]xfe,%al ▒
│ ↓ je 380 ▒
0.01 │ ↓ jle 2f0 ▒
0.01 │ cmp [=10=]xff,%al ▒
│ ↓ je 3a0 ▒
0.02 │ cmp [=10=]x1,%al ▒
│ ↓ jne 298 ▒
0.01 │ and [=10=]x10,%edx ▒
│ movl [=10=]x1,0x10(%rsp) ▒
│ movl [=10=]x1,0x1c8(%rbx) ▒
0.00 │ ↓ je 393
对应的源码在这里trace_lookup source code,如果我没看错的话,这条热路径cmp
指令对应的代码行数是296行,但不知道为什么这条线很慢,而且大部分时间都花钱?
命令 cmp %rsi,%r15
被标记为具有巨大的开销,因为它等待 mov 0x0(%rbp),%rsi
命令从缓存或内存中加载数据。该命令可能存在 L1 甚至 L2 缓存未命中。
对于代码片段
│ trace_lookup():
0.50 │ lea -0x1(%r12),%rdx
1.22 │ shr [=10=]x2b,%r13
0.37 │ and %r13,%rdx
0.57 │177: mov %rdx,%rbp
0.43 │ shl [=10=]x4,%rbp
1.33 │ add %rdi,%rbp
0.49 │ mov 0x0(%rbp),%rsi
24.40 │ cmp %rsi,%r15
│ ↓ jne 420
您有 24% 的当前函数的性能分析事件归因于 cmp 指令。采样分析的默认事件是“cycles”(CPU 时钟周期的硬件事件)或“cpu-clock”(线性时间的软件事件)。因此,大约 24% 的中断此功能的采样中断被报告用于此 cmp 命令的指令地址。分析和现代 Out-of-order CPUs 可能存在系统性偏差,当成本报告不是针对执行 运行 缓慢的命令,而是针对未完成执行(退休)的命令迅速地。如果 %rsi 寄存器值不等于 %r15 寄存器值,则此 cmp+jne 命令对(融合 uop)将更改程序的指令流。在古代,这样的命令应该只读取两个寄存器并比较它们的值,速度很快,不应该占用函数执行时间的 1/4。但是现代 CPU 寄存器不仅仅是存储值的 32 位或 64 位位置,它们还有一些用于 Out-of-order 引擎的隐藏标志(或重命名技术)。在您的示例中,mov 0x0(%rbp),%rsi
确实更改了 %rsi 寄存器。该命令通过地址 *%rbp 从内存中加载。 CPU 确实开始将此加载到 cache/memory 子系统中并将 %rsi 寄存器标记为“从内存加载挂起”,继续执行指令。下一条指令有可能不需要该加载的结果(这需要一些时间,例如 Haswell:L1 命中 4 cpu 个周期,L2 命中 12 个,L3 命中 36-66 个,以及额外的 50-100 ns 用于缓存未命中和 RAM 读取)。但是在你的情况下,下一条指令是 cmp+jne 并从 %rsi 读取,并且在将内存中的数据写入 %rsi 之前,该指令无法完成(CPU 可能会在 cmp+jne 执行过程中阻塞或多次重新启动该命令)。因此, cmp 有 24% 的开销,因为 mov 确实错过了最近的缓存。使用更高级的计数器,您可以估计它错过了哪个缓存,以及哪个 cache/memory 层最常为请求提供服务。
The corresponding source code is here trace_lookup source code, If I read correctly, the number of lines of code corresponding to this hot path cmp instruction is line 296, but I don't know why this line is so slow and cost most of the time?
使用如此短的 asm 片段,很难在 trace_lookup 的源代码中找到相应的代码行,也很难找到什么值以及为什么不在 L1/L2 缓存中。您应该尝试编写简短的可重现示例。
我正在尝试使用 libunwind(使用 linux perf)进行分析,perf top
监控目标进程,我得到了这个组装时间成本屏幕:
0.19 │ mov %rcx,0x18(%rsp) ▒
│ trace_lookup(): ▒
1.54 │ mov 0x8(%r9),%rcx ▒
│ _ULx86_64_tdep_trace(): ▒
0.52 │ and [=10=]x1,%edx ◆
0.57 │ mov %r14d,0xc(%rsp) ▒
0.40 │ mov 0x78(%rsp),%r10 ▒
1.24 │ sub %rdx,%r15 ▒
│ trace_lookup(): ▒
0.35 │ shl %cl,%r12d ▒
│ _ULx86_64_tdep_trace(): ▒
2.18 │ mov 0x90(%rsp),%r8 ▒
│ trace_lookup(): ▒
0.46 │ imul %r15,%r13 ▒
│ _ULx86_64_tdep_trace(): ▒
0.59 │ mov %r15,0x88(%rsp) ▒
│ trace_lookup(): ▒
0.50 │ lea -0x1(%r12),%rdx ▒
1.22 │ shr [=10=]x2b,%r13 ▒
0.37 │ and %r13,%rdx ▒
0.57 │177: mov %rdx,%rbp ▒
0.43 │ shl [=10=]x4,%rbp ▒
1.33 │ add %rdi,%rbp ▒
0.49 │ mov 0x0(%rbp),%rsi ▒
24.40 │ cmp %rsi,%r15 ▒
│ ↓ jne 420 ▒
│ _ULx86_64_tdep_trace(): ▒
2.10 │18e: movzbl 0x8(%rbp),%edx ▒
3.68 │ test [=10=]x8,%dl ▒
│ ↓ jne 370 ▒
1.27 │ mov %edx,%eax ▒
0.06 │ shl [=10=]x5,%eax ▒
0.73 │ sar [=10=]x5,%al ▒
1.70 │ cmp [=10=]xfe,%al ▒
│ ↓ je 380 ▒
0.01 │ ↓ jle 2f0 ▒
0.01 │ cmp [=10=]xff,%al ▒
│ ↓ je 3a0 ▒
0.02 │ cmp [=10=]x1,%al ▒
│ ↓ jne 298 ▒
0.01 │ and [=10=]x10,%edx ▒
│ movl [=10=]x1,0x10(%rsp) ▒
│ movl [=10=]x1,0x1c8(%rbx) ▒
0.00 │ ↓ je 393
对应的源码在这里trace_lookup source code,如果我没看错的话,这条热路径cmp
指令对应的代码行数是296行,但不知道为什么这条线很慢,而且大部分时间都花钱?
命令 cmp %rsi,%r15
被标记为具有巨大的开销,因为它等待 mov 0x0(%rbp),%rsi
命令从缓存或内存中加载数据。该命令可能存在 L1 甚至 L2 缓存未命中。
对于代码片段
│ trace_lookup():
0.50 │ lea -0x1(%r12),%rdx
1.22 │ shr [=10=]x2b,%r13
0.37 │ and %r13,%rdx
0.57 │177: mov %rdx,%rbp
0.43 │ shl [=10=]x4,%rbp
1.33 │ add %rdi,%rbp
0.49 │ mov 0x0(%rbp),%rsi
24.40 │ cmp %rsi,%r15
│ ↓ jne 420
您有 24% 的当前函数的性能分析事件归因于 cmp 指令。采样分析的默认事件是“cycles”(CPU 时钟周期的硬件事件)或“cpu-clock”(线性时间的软件事件)。因此,大约 24% 的中断此功能的采样中断被报告用于此 cmp 命令的指令地址。分析和现代 Out-of-order CPUs 可能存在系统性偏差,当成本报告不是针对执行 运行 缓慢的命令,而是针对未完成执行(退休)的命令迅速地。如果 %rsi 寄存器值不等于 %r15 寄存器值,则此 cmp+jne 命令对(融合 uop)将更改程序的指令流。在古代,这样的命令应该只读取两个寄存器并比较它们的值,速度很快,不应该占用函数执行时间的 1/4。但是现代 CPU 寄存器不仅仅是存储值的 32 位或 64 位位置,它们还有一些用于 Out-of-order 引擎的隐藏标志(或重命名技术)。在您的示例中,mov 0x0(%rbp),%rsi
确实更改了 %rsi 寄存器。该命令通过地址 *%rbp 从内存中加载。 CPU 确实开始将此加载到 cache/memory 子系统中并将 %rsi 寄存器标记为“从内存加载挂起”,继续执行指令。下一条指令有可能不需要该加载的结果(这需要一些时间,例如 Haswell:L1 命中 4 cpu 个周期,L2 命中 12 个,L3 命中 36-66 个,以及额外的 50-100 ns 用于缓存未命中和 RAM 读取)。但是在你的情况下,下一条指令是 cmp+jne 并从 %rsi 读取,并且在将内存中的数据写入 %rsi 之前,该指令无法完成(CPU 可能会在 cmp+jne 执行过程中阻塞或多次重新启动该命令)。因此, cmp 有 24% 的开销,因为 mov 确实错过了最近的缓存。使用更高级的计数器,您可以估计它错过了哪个缓存,以及哪个 cache/memory 层最常为请求提供服务。
The corresponding source code is here trace_lookup source code, If I read correctly, the number of lines of code corresponding to this hot path cmp instruction is line 296, but I don't know why this line is so slow and cost most of the time?
使用如此短的 asm 片段,很难在 trace_lookup 的源代码中找到相应的代码行,也很难找到什么值以及为什么不在 L1/L2 缓存中。您应该尝试编写简短的可重现示例。