首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
299 阅读
2
从零开始配置 vim(15)——状态栏配置
175 阅读
3
nvim番外之将配置的插件管理器更新为lazy
164 阅读
4
2018总结与2019规划
154 阅读
5
wsl 启动报错:Wsl/0x80080005
129 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
emacs
elisp
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
读书笔记
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
468
篇文章
累计收到
33
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
1
篇与
的结果
2026-10-30
《elisp 每日一练》——性能分析
在前面我们讨论了关于 elisp 的调试和异常相关的内容,关于工程化的内容还需要了解如何对自己的elisp配置进行性能分析,尝试解决某些影响使用的卡顿问题背景知识如果我们发现自己的elisp 代码执行卡顿,一般需要经历下面几个步骤来确定范围、分析问题、以及最后解决。首先可以使用emacs 内置的 Profiler 机制确定问题所在。该机制的启用配套使用的函数有 profiler-start、profiler-report、profiler-stop。一般使用的步骤如下:当你发现执行某个命令卡顿严重时,可以先调用 profiler-start 开启性能检测。它的签名如下(profiler-start MODE)MODE 参数可以是 'cpu、'memory 或者 'cpu+mem 之后我们可以执行导致卡顿的函数或者操作。接着调用 profilter-report 停止采样并弹出一个类似火焰图/调用树的剖析报告缓冲区。 分析完毕之后可以调用 profiler-stop 停止Emacs内部的采样,避免持续开销。我们以一个例子来说明如何解读数据(defun my-inefficient-string-join (n) (let ((res "")) (dotimes (i n) (setq res (concat res (number-to-string i)))) res)) (defun my-test-heavy-profiling-task() (interactive) (profiler-start 'cpu+mem) ;; 调用函数执行任务 (my-inefficient-string-join 5000) ;;停止任务并展示报告 (profiler-report) (profiler-stop))我们可以得到两个界面,一个显示cpu耗时、一个显示内存,它们的内容如下:46,779,444 99% - command-execute 46,779,444 99% - funcall-interactively 46,779,444 99% - execute-extended-command 46,779,444 99% - command-execute 46,779,444 99% - funcall-interactively 46,779,444 99% - my-test-heavy-profiling-task 45,218,433 96% + my-inefficient-string-join 1,560,380 3% profiler-report 631 0% profiler-start 46,984 0% Automatic GC 0 0% ... 46 100% Automatic GC 0 0% ...从这里我们可以看出耗时操作主要是调用 my-inefficient-string-join 它占据了96%的CPU时间。前面带有 + 的可以按 tab 进一步展开。如果我们进一步展开就可以发现,函数内部的主要耗时是调用 concat 函数。确定了可疑函数,我们就可以调用 elp 相关函数监控这两个函数的具体调用情况:定义一个存储函数符号的list 变量,表示需要监控哪些函数elp-instrument-list 开始监控,可以传入上一步定义的列表变量,或者默认使用 elp-function-list 中保存的函数elp-results 查看监控结果最后调用 elp-restore-function 恢复部分函数或者调用 elp-restore-all 恢复所有函数因此我们可以修改一下上述的函数,重点监控 my-inefficient-string-join(defun my-test-heavy-profiling-task() (interactive) (setq elp-function-list '(my-inefficient-string-join)) (elp-instrument-list elp-function-list) ;; 调用函数执行任务 (my-inefficient-string-join 5000) ;;停止任务并展示报告 (elp-results) (elp-restore-all))执行之后我们可以发现调用结果如下::my-inefficient-string-join 1 0.934222 0.934222 concat 5000 0.8685359999 0.0001737071结合具体代码来看,主要的消耗在 my-inefficient-string-join 内部调用 concat。concat 每一次的消耗可能并不多。事情真的是这样吗?elp 无法统计每一次调用concat 真实的消耗,上面显示的只是平均每次的消耗。也就是说它是通过 0.8685359999 / 5000 得到的平均值。实际上我们知道 concat 的实现机制是每次追加都会经历:分配新内存-->拷贝原始字符串到新内存-->删除旧的内存块-->追加字符。也就是说,随着原字符串越来越大,每次拷贝的开销会越来越大。也就说随着调用次数的增加,它的时间复杂度是 O(n²)。 为了验证这个,我们可以使用 benchmark-run 函数,它可以按照需求多次调用我们的函数,并输出调用结果。它的签名如下:(benchmark-run &optional REPETITIONS &rest FORMS)REPETITIONS 是一个数字表示应该运行后面的 FORMS 多少次。输出的结果是一个形如 (总耗时(秒) GC执行次数 GC总耗时(秒)) 的列表我们可以分别执行下面的测试代码(benchmark-run 1 (my-inefficient-string-join 1000)) (benchmark-run 1 (my-inefficient-string-join 2000)) (benchmark-run 1 (my-inefficient-string-join 4000))结果如下:(0.057176 2 0.05415199999999931) (0.159746 8 0.15689200000000003) (0.649431 36 0.6428240000000001)我们发现主要开销在GC上,调用次数是2倍增长,但是GC次数大概是 4倍增长符合我们上面提到的 O(n²) 的时间复杂度。所以对于这种情况我们不应该使用 concat 来连接。其实对于这种需要频繁插入新元素的场景,正确的做法是使用 list 作为基础数据结构。我们的上述代码可以改为:(defun my-efficient-string-join (n) (let ((lst '())) (dotimes (i n) (push (number-to-string i) lst)) (string-join (nreverse lst) "")))最终再次执行 benchmark-run 得到的结果如下:(0.032276 1 0.031180999999999237) (0.001953 0 0.0) (0.028748 1 0.02522500000000072)我们发现它的开销不再随着循环次数的上升而显著上升了。总结本篇我们通过一个例子,讨论了进行性能分析的一般方法。在此对上述方法进行总结:可以通过 profiler 机制分析某个操作具体卡在哪个函数, 配套的函数有:profiler-start、profiler-report、profiler-stop。然后通过 elp 系列统计函数的调用次数和消耗情况,对应的函数是: elp-instrument-list、elp-results、elp-restore-function 和 elp-restore-all最后针对具体函数可以使用 benchmark-run 单独多次执行表达式,获取性能信息
2026年10月30日
1 阅读
0 评论
0 点赞