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.html、LLDB 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
.
我编写了一个脚本,为我的 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.html、LLDB 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
.