在上一篇04优化钩子事件处理中,我们在钩子函数中引入了call和return事件的处理,对性能进行了优化。

细心的同学可能已经发现了,我们的hook函数中call事件和line都需要对整个断点表进行遍历,这其中其实是存在着一些冗余的。因为call事件只关心函数是否有断点,而line事件则只关心本函数内有哪些断点。所以我们可以想办法优化一下断点信息的数据结构,进一步提升性能。

源码已经上传Github,欢迎watch/star。

本博客已迁移至CatBro's Blog,那里是我自己搭建的个人博客,欢迎关注。

实现分析

原来的断点表status.bptable是以断点id为键的数组,我们需要通过断点id来快速删除断点,所以status.bptable表还是保留。

但是在钩子函数中,我们并不关心断点的id,相反我们关心断点属于哪个函数。只有当程序执行到有断点的函数时,我们才需要处理line事件。所以,很自然地可以想到,我们应该新增一个以函数为键的断点表。我们把这个新表定义如下:

status.funcbpt = {}     -- 以函数为键的断点表

那么新表的值应该是什么呢?因为一个函数中可能有多个断点,而在line事件中我们又需要比较当前行是否是断点行,所以我们把新表status.funcbpt的值也设计成一个表,该表的键是断点行号,值为断点id。因为断点行号并不连续,所以status.funcbpt[func]表不是一个序列,为了快速获取断点个数,我们在表中额外加了一个特殊的num字段保存该函数中的断点个数。

这样我们就可以在call事件中快速地判断当前函数是否有断点,在line事件中快速地判断当前行是否是断点行。

另外,之所以将断点id作为status.funcbpt[func]表的值,则是为了保留从status.funcbpt访问status.bptable中对应断点的能力(status.bptable表中元素因为包含了funcline字段,所以也可以访问到status.funcbpt对应断点)。

这么说可能不直观,我们来看个例子。

假设我们的bptable表中func函数添加了两个断点如下:

bptable[1] = {func = func, line = 10}
bptable[2] = {func = func, line = 20}

对应的在funcbpt表中的操作如下:

funcbpt[func] = {}          -- 构造表
funcbpt[func][10] = 1 -- 函数func,行号10,断点id为1
funcbpt[func].num = 1 -- 该函数第一个断点
funcbpt[func][20] = 2 -- 函数func,行号20,断点id为2
funcbpt[func].num = funcbpt[func].num + 1 -- 断点个数+1

OK,理清楚了status.funcbpt的数据结构设计,实现起来就简单了。

添加断点

我们先从设置断点函数入手修改代码。新增的代码已经用中括号括起来了,有两个部分。其中前面的部分先根据函数和行号检查是否已经设置过同一个断点了,如果是的话,直接返回之前设置的断点id。

后面的部分,在断点保存到s.bptable表之后,也要保存到我们新增的表s.funcbpt表中,行号line作为键,新断点id作为值。如果设置的是该函数的第一个断点,需要先进行表的初始化构造,所以操作稍有不同。

local function setbreakpoint(func, line)
local s = status
if type(func) ~= "function" or type(line) ~= "number" then
return nil
end
[[ 新增代码开始 ]]
-- 已经设置了相同的断点
if s.funcbpt[func] and s.funcbpt[func][line] then
return s.funcbpt[func][line]
end
[[ 新增代码结束 ]]
s.bpid = s.bpid + 1
s.bpnum = s.bpnum + 1
s.bptable[s.bpid] = {func = func, line = line}
[[ 新增代码开始 ]]
if s.funcbpt[func] then -- 该函数已经有断点了
s.funcbpt[func].num = s.funcbpt[func].num + 1
s.funcbpt[func][line] = s.bpid
else -- 该函数第一个断点
s.funcbpt[func] = {}
s.funcbpt[func].num = 1
s.funcbpt[func][line] = s.bpid
end
[[ 新增代码结束 ]]
if s.bpnum == 1 then -- 全局第一个断点
debug.sethook(hook, "c") -- 设置钩子"call"事件
end
return s.bpid --> 返回断点id
end

删除断点

相应地,在删除断点的时候,我们也需要把s.funcbpt表中对应的断点删除。首先根据断点id,从s.bptable表中获取到断点的函数和行号,从而找到s.funcbpt表中对应的断点。现将断点个数减1,然后将对应断点删除(防止后续line事件找到该断点)。如果该函数已经没有断点了,那么将s.funcbpt[func]表本身也删除(防止后续call事件以为该函数还有断点)。

local function removebreakpoint(id)
local s = status
if s.bptable[id] == nil then
return
end
[[ 新增代码开始 ]]
local func = s.bptable[id].func
local line = s.bptable[id].line
s.funcbpt[func].num = s.funcbpt[func].num - 1
s.funcbpt[func][line] = nil
if s.funcbpt[func].num == 0 then
s.funcbpt[func] = nil
end
[[ 新增代码结束 ]]
s.bptable[id] = nil
s.bpnum = s.bpnum - 1
if s.bpnum == 0 then
debug.sethook() -- 移除钩子
end
end

钩子函数

然后来修改钩子函数的处理,钩子的函数的修改主要有两处,分别是call事件和line事件。我们先来看call事件的修改:

 local function hook (event, line)
local s = status
if event == "call" or event == "tail call" then
local func = debug.getinfo(2, "f").func
- for _, v in pairs(s.bptable) do
- -- found breakpoint in current function
- if v.func == func then
- if event == "call" then
- s.stackdepth = s.stackdepth + 1
- end
- s.stackinfos[s.stackdepth] =
- {func = func, hasbreak = true}
- debug.sethook(hook, "crl") -- 添加"line"事件
- return
- end
- end
- -- 没有断点
- if event == "call" then
+ if event == "call" then -- 对于尾调用,直接覆盖
s.stackdepth = s.stackdepth + 1
end
- s.stackinfos[s.stackdepth] = {func = func, hasbreak = false}
- debug.sethook(hook, "cr") -- 去掉"line"事件
+ -- found breakpoint in current function
+ if s.funcbpt[func] then
+ s.stackinfos[s.stackdepth] = {func = func, hasbreak = true}
+ debug.sethook(hook, "crl") -- 添加"line"事件
+ else -- no breakpoints found
+ s.stackinfos[s.stackdepth] = {func = func, hasbreak = false}
+ debug.sethook(hook, "cr") -- 去掉"line"事件
+ end
elseif event == "return" or event == "tail return" then

主要的改动就是从原本的遍历s.bptable表来查找是否有断点,改成了直接通过检查s.funcbpt[func]是否不为nil来判断是否有断点。这里直接从遍历一个表,优化成了一个查表操作。

第二处是line事件的修改,下面是修改前的代码:

    -- 省略
elseif event == "line" then
for _, v in pairs(s.bptable) do
if v.func == s.stackinfos[s.stackdepth].func
and v.line == line then
if not s.funcinfos[v.func] then
s.funcinfos[v.func] = debug.getinfo(2, "nS")
end
local info = s.funcinfos[v.func]
local prompt = string.format("%s (%s)%s %s:%d\n",
info.what, info.namewhat, info.name, info.short_src, line)
io.write(prompt)
debug.debug()
end
end
end
-- 省略

下面是修改后的代码,除了增加了一些局部变量简化代码之外,也是将原本的遍历s.bptable表来判断当前行是否是断点行,改成了直接通过检查s.funcbpt[curfunc][line]是否不为nil来判断当前行是否是断点行。这里也将遍历一个表的动作,优化成了一个查表操作。

    elseif event == "line" then
local curfunc = s.stackinfos[s.stackdepth].func
local funcbp = s.funcbpt[curfunc]
assert(funcbp)
if funcbp[line] then
if not s.funcinfos[curfunc] then
s.funcinfos[curfunc] = debug.getinfo(2, "nS")
end
local info = s.funcinfos[curfunc]
local prompt = string.format("%s (%s)%s %s:%d\n",
info.what, info.namewhat, info.name, info.short_src, line)
io.write(prompt)
debug.debug()
end
end

复杂度分析

我们仍然假设代码执行的总行数为L,断点数N=n*b,其中n为有断点的函数个数,b为平均每个函数的断点数,断点所在函数平均行数为l,断点所在函数平均调用次数为c,总的函数调用次数C

完全没优化前复杂度为O(L*N),上一篇的事件处理优化后的复杂度为O(C*N+c*l*N),而本篇的数据结构优化之后复杂度进一步缩小为O(C+c*l)

测试多函数多断点

先来测试一下修改之后断点功能是否正常,编写一个测试脚本如下。我们对foo函数和bar函数分别添加了两个断点,其中foo函数第一个断点添加了两次用于测试重复添加的情况。

local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint
pv = ldb.printvarvalue
sv = ldb.setvarvalue
ptb = ldb.printtraceback local function foo ()
local a = 1
end local function bar ()
local b = 1
end local id1 = setbp(foo, 9)
assert(id1 == 1)
local id1 = setbp(foo, 9)
assert(id1 == 1)
local id2 = setbp(foo, 10) local id3 = setbp(bar, 13)
local id4 = setbp(bar, 14) foo()
bar() rmbp(id1)
rmbp(id2)
rmbp(id3)
rmbp(id4) foo()
bar()

运行脚本,4个断点情况都能正常跑到。删除断点后再调用foo和bar函数,不再碰到断点。

$ lua test.lua
Lua (local)foo test.lua:9
lua_debug> cont
Lua (local)foo test.lua:10
lua_debug> cont
Lua (local)bar test.lua:13
lua_debug> cont
Lua (local)bar test.lua:14
lua_debug> cont
$

性能比对

我们再来做个简单的测试,看看我们优化的效果。编写如下测试脚本,foo函数模拟较长的程序,然后在另一个函数上加个断点(为了设置hook)。我们分别使用优化前的luadebug.lua和优化后的luadebug.lua进行测试。

local ldb = require "luadebug"
local setbp = ldb.setbreakpoint
local rmbp = ldb.removebreakpoint local function foo ()
local a = 1
for i = 1, 10000000 do
a = a + 1
end
end local function bar ()
end local id1 = setbp(bar, 13) foo()

优化前

使用优化前的实现,运行这个脚本用了40s

$ time lua test2.lua
lua test2.lua 39.54s user 0.16s system 99% cpu 39.957 total

优化后

使用优化后的实现,则只花了0.109s,相差了接近400倍,可见我们的优化效果还是很明显的。

$ time lua test2.lua
lua test2.lua 0.10s user 0.00s system 97% cpu 0.109 total

Lua中如何实现类似gdb的断点调试--05优化断点信息数据结构的更多相关文章

  1. Lua中如何实现类似gdb的断点调试—07支持通过函数名称添加断点

    我们之前已经支持了通过函数来添加断点,并且已经支持了行号的检查和自动修正.但是通过函数来添加断点有一些限制,如果在当前的位置无法访问目标函数,那我们就无法对其添加断点. 于是,本篇我们将扩展断点设置的 ...

  2. Lua中如何实现类似gdb的断点调试--01最小实现

    说到Lua代码调试,最常用的方法应该就是加一堆print进行打印.print大法虽好,但其缺点也是显而易见的.比如效率低下,需要修改原有函数内部代码,在每个需要的地方添加print语句,运行一次只能获 ...

  3. Lua中如何实现类似gdb的断点调试—09支持动态添加和删除断点

    前面已经支持了几种不同的方式添加断点,但是必须事先在代码中添加断点,在使用上不是那么灵活方便.本文将支持动态增删断点,只需要开一开始引入调试库即可,后续可以在调试过程中动态的添加和删除断点.事不宜迟, ...

  4. Lua中如何实现类似gdb的断点调试--02通用变量打印

    在前一篇01最小实现中,我们实现了Lua断点调试的的一个最小实现.我们编写了一个模块,提供了两个基本的接口:设置断点和删除断点. 虽然我们已经支持在断点进行变量的打印,但是需要自己指定层数以及变量索引 ...

  5. Lua中如何实现类似gdb的断点调试—08支持通过包名称添加断点

    在前一篇中我们支持了通过函数名称来添加断点,我们同时也提到了在Lua中一个函数的名称的并不是确定的.准确的说,Lua中的函数并没有名称,所谓名称其实是保存这个函数值的变量的名称. 于是通过函数名称添加 ...

  6. Lua中如何实现类似gdb的断点调试--04优化钩子事件处理

    在第一篇的01最小实现中,我们实现了一个断点调试的最小实现,在设置钩子函数时只加了line事件,显然这会对性能有很大的影响.而后来两篇02通用变量打印和03通用变量修改及调用栈回溯则是提供了一些辅助的 ...

  7. Lua中如何实现类似gdb的断点调试—06断点行号检查与自动修正

    前面两篇我们对性能做了一个优化,接下来继续来丰富调试器的特性. 我们前面提到过,函数内并不是所有行都是有效行,空行和注释行就不是有效行.我们之前在添加断点的时候,并没有对行号进行检查,任何行号都能成功 ...

  8. Lua中如何实现类似gdb的断点调试--03通用变量修改及调用栈回溯

    在前面两篇01最小实现及02通用变量打印中,我们已经实现了设置断点.删除断点及通用变量打印接口. 本篇将继续新增两个辅助的调试接口:调用栈回溯打印接口.通用变量设置接口.前者打印调用栈的回溯信息,后者 ...

  9. 2018-8-10-VisualStduio-打断点调试和不打断点调试有什么区别

    title author date CreateTime categories VisualStduio 打断点调试和不打断点调试有什么区别 lindexi 2018-08-10 19:16:52 + ...

随机推荐

  1. 一步一步搭建基于ffmpeg和sdl2的流媒体播放器

    一.  背景: 一步一步从资料收集.技术选型.代码编写.性能优化,动手搭建一款支持rtsp.rtmp等常用流媒体格式的视频播放器,ffmpeg用于流媒体解码,sdl2用于视频画面渲染和声音播放. 二. ...

  2. 在Android中用纯Java代码布局

    感谢大佬:https://www.jianshu.com/p/7aedea560f16 在Android中用纯Java代码布局 本文的完成了参考了一篇国外的教程,在此表示感谢. Android中的界面 ...

  3. 使用ajax上传文件

    1. XMLHttpRequest(原生ajax) [](javascript:void(0) <input class="file" type="file&quo ...

  4. 简单的MAC的终端命令

    今天小研究了一下MAC的终端命令,主要为了方便调试程序用,XCODE用不来啊... 在这里记下..防止丢失 pwd 当前工作目录 cd(不加参数) 进root cd(folder) 进入文件夹 cd ...

  5. 利用redis+AOP简单处理MQ冥等问题

    思路: 1.利用redis内部的串行执行特性,使用getandset()处理分布式问题; 2.注解提供入参选择,通过数据抽取后计算MD5值,实现业务性值的冥等: 代码区: 1.注解 1 /** 2 * ...

  6. linxu 查看运行日志

    journalctl - 检索 systemd 日志 journalctl 可用于检索 systemd(1) 日志(由 systemd-journald.service(8) 记录). 如果不带任何参 ...

  7. Solution Set - Border Theory

      我发现写 Solution Set 就不用写每道题的题意了,岂不美哉?   首先是一些奇妙结论.   定理 1(弱周期定理) 对于字符串 \(S\),若 \(S[:p]\) 和 \(S[:q]\) ...

  8. n次单位根(n-th unit root)

    最近在看CKKS方案,里面的编码/解码用到了n次单位根,感觉基于环上的加密,很多都会用到,现在系统的学习一下! 定义 先看定义: \[z^n=1,(n=1,2,3,...) \] 该方程的根z为n次单 ...

  9. 《深度探索C++对象模型》第一章 | 关于对象

    C++对象模式 非静态数据成员放置在每个类对象内,静态数据成员则被放置在所有类对象之外.静态和非静态的成员函数也被放置在所有类对象之外.每个类产生一堆指向虚函数的指针,放在虚表(vtbl)中.每个类对 ...

  10. CPU优化上下文切换之线程上下文切换案例分析

    对于线程上下文切换,如果同进程内就是只是线程上下文切换,如果非同进程内则是进程上下文切换.下面进行线程上下文切换场景模拟. 一.环境准备~模拟工具sysbench. 1)安装git yum -y in ...