【问题标题】:Perf startup overhead: Why does a simple static executable which performs MOV + SYS_exit have so many stalled cycles (and instructions)?Perf 启动开销:为什么执行 MOV + SYS_exit 的简单静态可执行文件有这么多停滞的周期(和指令)?
【发布时间】:2018-07-26 08:01:18
【问题描述】:

我正在尝试了解如何衡量性能并决定编写一个非常简单的程序:

section .text
    global _start

_start:
    mov rax, 60
    syscall

我用perf stat ./bin 运行程序令我惊讶的是stalled-cycles-frontend 太高了。

      0.038132      task-clock (msec)         #    0.148 CPUs utilized          
             0      context-switches          #    0.000 K/sec                  
             0      cpu-migrations            #    0.000 K/sec                  
             2      page-faults               #    0.052 M/sec                  
       107,386      cycles                    #    2.816 GHz                    
        81,229      stalled-cycles-frontend   #   75.64% frontend cycles idle   
        47,654      instructions              #    0.44  insn per cycle         
                                              #    1.70  stalled cycles per insn
         8,601      branches                  #  225.559 M/sec                  
           929      branch-misses             #   10.80% of all branches        

   0.000256994 seconds time elapsed

据我了解stalled-cycles-frontend 这意味着 CPU 前端必须等待某些操作(例如总线事务)的结果完成。

那么在最简单的情况下,是什么导致 CPU 前端大部分时间都在等待呢?

还有 2 个页面错误?为什么?我没有读取任何内存页。

【问题讨论】:

  • 47654条指令你怎么不担心? :) 不确定 perf 到底计算了什么,但大概它正在执行的其他任何东西(内核代码?)也是造成停顿的原因。
  • 如果你复制/粘贴你的真实代码(和构建指令),它一开始就不可能存在。总是这样做而不是重新输入代码。如果您正在编写代码并且尚未在任何地方对其进行过测试,请仅在 SO 中键入代码。
  • @PeterCordes 正是缺少的_start: 标签让我认为执行了正常的初始化。此外,在如何抑制 libc 链接的问题中也缺少描述:如果您没有明确尝试这样做,那么如果没有这些东西,就很难生成可执行文件。无论如何,不​​管我的猜测是否正确,我都不会删除我的评论:链接真的很好,其他读者也可能很感兴趣。
  • @cmaster: ld foo.o -o foo 在 Linux 上生成静态可执行文件。不,动态链接的原因不是对内核头文件的依赖(用户空间 ABI 是稳定的,你不需要不同的 glibc 来匹配你的内核)。原因是 libc 没有嵌入到每个二进制文件中,并且 glibc 错误修复不需要重新编译 everything。加上通常的内存优势。使用 gcc,要使用自定义 _start 创建静态可执行文件,请使用 gcc foo.o -nostdlib -static。或者将 libc 与您自己的 _startgcc foo.o -nostartfiles 动态链接(glibc init 函数仍然运行)。
  • @cmaster:glibc init 函数使用与在动态加载的 C++ 库中运行构造函数相同的机制运行:libc.so.6_global_ctors 数组或类似的东西中具有必要的 init 函数的地址,动态链接器在跳转到_start 之前调用它。如果 gcc 告诉它,链接器只会“插入代码以...调用main”(即 CRT(C 运行时)启动文件)。如果您运行gcc -v foo.c,您将看到编译器、汇编器和链接器命令行(ld 通过collect2 包装器调用,但您可以看到crt0.ogcc 显式传递)

标签: linux performance assembly x86-64 perf


【解决方案1】:

页面错误包括代码页。

perf stat 包括启动开销。

IDK 了解perf 如何开始计数的详细信息,但大概它必须在内核模式下对性能计数器进行编程,所以它们在计数时 CPU 切换回用户模式(停顿很多周期,尤其是在具有使 TLB 无效的 Meltdown 防御的内核上)。

我猜大部分47,654 记录的指令都是内核代码。也许包括页面错误处理程序!

我猜你的进程永远不会进入用户->内核->用户,整个过程是内核->用户->内核(启动,syscall调用sys_exit,然后永远不会返回用户空间),所以无论如何,TLB 永远不会变热,除非在sys_exit 系统调用之后在内核中运行时。无论如何,TLB 未命中并不是页面错误,但这可以解释很多停滞的周期。

用户->内核转换本身解释了大约 150 个停滞周期,顺便说一句。 syscall 比缓存未命中更快(除了它没有流水线化,实际上会刷新整个流水线;即特权级别未重命名。)

【讨论】:

  • 这是一个快速的部分答案;我想看到一个答案,它准确地解释了哪些(可能是内核)指令包含在perf stat 被视为启动/关闭开销的一部分。如果你能控制它;我认为 x86 性能计数器可以编程为仅在用户模式或内核 + 用户模式下计数。
  • 按照你的建议,我跑了perf record -e dTLB-load-misses。它计算了10519 事件。最上面是以下行35.55% perf [kernel.kallsyms]。这是否意味着在这种简单的情况下,计算 TLB 未命中开销的性能很重要?
  • @St.Antario:显然是的。但是,对于 47k 指令,您有 10k TLB 未命中?这听起来难以置信的高。听起来我们需要一个真正的答案来回答这个问题,而不仅仅是我对perf 挥手致意,包括一些启动开销。
  • 但是如果perf 启动开销太高......我们如何使用它编写微/纳米基准?有可能吗?
  • @St.Antario:将您的代码放入一个至少运行 50 毫秒或其他时间的循环中。我有一个testloop.asm 和一个 assemble + link + perf stat 命令行,我可以很容易地找到它使用mov ebp, 1000000000 作为dec ebp / jnz 循环的循环计数器。如果循环中的代码不止几个快速微指令,我可以根据需要添加或删除零。使用perf stat -r4,我获得了非常好的可重复性和准确性,预期结果在 10k 或 100k 的 1 部分内,运行时间为 0.1 到 1 秒左右。见stackoverflow.com/questions/44169342
猜你喜欢
  • 1970-01-01
  • 2014-04-05
  • 2014-07-26
  • 1970-01-01
  • 2020-08-16
  • 2011-11-30
  • 1970-01-01
  • 2014-08-08
  • 2018-08-25
相关资源
最近更新 更多