LLDB 断点性能——我应该期待什么?

LLDB Breakpoints performance - what should I expect?

我编写了一个脚本,为我的 iOS 项目添加了很多断点。每个断点都有一个调用一些日志代码并继续而不停止的命令。

在我的项目执行过程中,这些断点每秒被调用数十次甚至数百次。 不幸的是,应用程序性能在添加这些断点后崩溃了。它几乎没有响应,因为执行断点会减慢速度。

我的问题是:这正常吗?断点的性能开销这么大吗?

我在下方粘贴来自 ~/.lldb 的 python 脚本的一部分:

...
for funcName in funcNames:
   breakpointCommand = f'breakpoint set -n {funcName} -f {fileName}'
   lldb.debugger.HandleCommand(breakpointCommand)
   lldb.debugger.HandleCommand('breakpoint command add --script-type python --python-function devTrackerScripts.breakpoint_callback')
def breakpoint_callback(frame, bp_loc, dict):
   lineEntry = frame.GetLineEntry()
   functionName = frame.GetDisplayFunctionName()
   expression = f'expr -- proofLog(lineEntry: "{lineEntry}", function: "{functionName}")'

   lldb.debugger.HandleCommand(expression)

   return False

断点难做高性能。

它们涉及在调试过程中发生异常,然后将上下文切换到调试器以处理异常,然后单步执行原始指令,这涉及更多的上下文切换和单步之后的另一个异常。然后另一个设置流程再次进行。当您调试 iOS 设备时,为所有上下文切换添加从 iOS 设备到您 Mac 的流量。

在您的情况下,您还在每个站点调用一个函数,这意味着编译表达式,将其下载到进程和 运行 代码。

看看是遇到断点导致了大部分减速,还是表达式求值导致了你的减速,这将是一件很有趣的事情。如果主要是表达式计算,那么也许您可以想出另一种方法来实现这种效果?

在 Linux 上本地调试时遇到相应的断点后,默认设置 运行 的 lldb 在处理断点回调时似乎比 gdb 慢(lldb 10.0.0 vs gdb 9.1 on Ubuntu 20.04).

testcase.c

这是性能测量的测试用例。

#include <stdio.h>
int count = 0;
int fib(int n) {
    ++count;
    if(n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
int main() {
    printf("fib(16) = %d\n", fib(16));
    printf("count-1 = %d\n", count - 1);
}

可以用下面的命令编译得到可执行文件testcase.

clang -g testcase.c -o testcase

m_lldb.py

使用下面的代码创建一个 Python 脚本 m_lldb.py,用于在 fib() 处设置断点和断点回调 mbp_callback.

import lldb
import time
s, e = 0, 0
def mbp_callback(frame, bp_loc, dict):
    global s, e
    l, e = e, time.time()
    if s == 0: l, s = e, e
    print("Callback: %9.6fs,  Total: %9.6fs" % (e - l, e - s))
    return False
def __lldb_init_module(debugger, dict):
    tgt = debugger.GetSelectedTarget()
    bp = tgt.BreakpointCreateByName("fib")
    bp.SetScriptCallbackFunction('m_lldb.mbp_callback')

运行-lldb.sh

一旦可执行文件 testcase 和脚本 m_lldb.py 准备就绪,运行 下面的 bash 脚本用于测量 lldb 在处理断点回调方面的性能。

#!/bin/bash -x
cat << LLDBCMD > lldb.cmd
command script import m_lldb.py
breakpoint list
run
quit
LLDBCMD
lldb -s lldb.cmd -- ./testcase

m_gdb.py

现在让我们为 gdb 创建一个 Python 脚本,它具有与 lldb 相同的断点回调。

import gdb
import time
s, e = 0, 0
class MBreakpoint(gdb.Breakpoint):
    def stop(self):
        global s, e
        l, e = e, time.time()
        if s == 0: l, s = e, e
        print("Callback: %9.6fs,  Total: %9.6fs" % (e - l, e - s))
        return False
MBreakpoint("fib")

运行-gdb.sh

让我们运行下面的bash脚本来衡量gdb处理断点回调的性能。

#!/bin/bash -x
cat << GDBCMD > gdb.cmd
set pagination off
source m_gdb.py
info breakpoint
run
quit
GDBCMD
gdb -x gdb.cmd --args ./testcase

处理断点回调的性能测量结果

服务器信息

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04 LTS
Release:        20.04
Codename:       focal

$ lldb --version
lldb version 10.0.0

$ gdb --version
GNU gdb (Ubuntu 9.1-0ubuntu1) 9.1

$ grep -m1 "model name" /proc/cpuinfo
model name      : Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz

lldb 的结果

... ...
Callback:  0.001098s,  Total:  3.509685s
Callback:  0.001106s,  Total:  3.510791s
Callback:  0.001099s,  Total:  3.511890s
Callback:  0.001097s,  Total:  3.512987s
Callback:  0.001097s,  Total:  3.514084s
Callback:  0.001107s,  Total:  3.515191s
fib(16) = 987
count-1 = 3192
Process 2527525 exited with status = 0 (0x00000000)

gdb 的结果

... ...
Callback:  0.000182s,  Total:  0.594779s
Callback:  0.000188s,  Total:  0.594966s
Callback:  0.000182s,  Total:  0.595149s
Callback:  0.000189s,  Total:  0.595337s
Callback:  0.000184s,  Total:  0.595521s
Callback:  0.000187s,  Total:  0.595709s
fib(16) = 987
count-1 = 3192
[Inferior 1 (process 2527714) exited normally]

这表明 lldb 5.9x slower 比 gdb 在遇到相关断点时调用断点回调。

LLDB 的客户端-服务器架构似乎导致了上述在本地调试进程时的性能不佳。如 https://lldb.llvm.org/use/remote.htmlLLDB on Linux and macOS uses the remote debugging stub even when debugging a process locally 处的文档中所述。同时,lldb 中的繁重线程似乎是性能不佳的另一个原因。

在本地主机上使用 gdbserver 会使 gdb 运行 慢大约 2 倍,但它仍然比使用 lldb 在本地调试快得多。

运行-gdbsvr.sh

#!/bin/bash -x
(gdbserver --once localhost:2345 ./testcase) &
cat << GDBCMD > gdbsvr.cmd
target remote localhost:2345
set pagination off
source m_gdb.py
info breakpoint
continue
quit
GDBCMD
gdb -x ./gdbsvr.cmd --args ./testcase

gdbserver 的结果

... ...
Callback:  0.000384s,  Total:  1.236005s
Callback:  0.000387s,  Total:  1.236392s
Callback:  0.000383s,  Total:  1.236775s
Callback:  0.000386s,  Total:  1.237162s
Callback:  0.000383s,  Total:  1.237545s
Callback:  0.000384s,  Total:  1.237929s
fib(16) = 987
count-1 = 3192

Child exited with status 0

PS: 还要考虑断点回调对性能的影响。让我们用 lldb 和 gdb 测量回调执行的影响。

callback.py

import time
s, e = 0, 0
def stop():
    t = time.time()
    global s, e
    l, e = e, time.time()
    if s == 0: l, s = e, e
    print("Callback: %9.6fs,  Total: %9.6fs" % (e - l, e - s))
    return time.time() - t
for i in range(6):
    print("%9.6f" % stop())

运行 callback.py 与 lldb

(lldb) command script import callback.py
Callback:  0.000000s,  Total:  0.000000s
 0.000034
Callback:  0.000045s,  Total:  0.000045s
 0.000009
Callback:  0.000015s,  Total:  0.000060s
 0.000007
Callback:  0.000013s,  Total:  0.000073s
 0.000007
Callback:  0.000013s,  Total:  0.000086s
 0.000007
Callback:  0.000013s,  Total:  0.000099s
 0.000007
(lldb)

运行 callback.py 与 gdb

(gdb) source callback.py
Callback:  0.000000s,  Total:  0.000000s
 0.000023
Callback:  0.000033s,  Total:  0.000033s
 0.000010
Callback:  0.000018s,  Total:  0.000051s
 0.000010
Callback:  0.000017s,  Total:  0.000068s
 0.000009
Callback:  0.000017s,  Total:  0.000086s
 0.000009
Callback:  0.000018s,  Total:  0.000103s
 0.000009
(gdb)

每次执行断点回调大约需要 7~9us,在 lldb 上 运行 会快一点。执行断点回调对性能的影响非常有限,~4.8% for gdb~0.6% for lldb.