《elisp 每日一练》——性能分析

在前面我们讨论了关于 elisp 的调试和异常相关的内容,关于工程化的内容还需要了解如何对自己的elisp配置进行性能分析,尝试解决某些影响使用的卡顿问题

背景知识

如果我们发现自己的elisp 代码执行卡顿,一般需要经历下面几个步骤来确定范围、分析问题、以及最后解决。

首先可以使用emacs 内置的 Profiler 机制确定问题所在。该机制的启用配套使用的函数有 profiler-start、profiler-report、profiler-stop。

一般使用的步骤如下:

当你发现执行某个命令卡顿严重时,可以先调用 profiler-start 开启性能检测。它的签名如下

emacs-lisp 复制代码
(profiler-start MODE)

MODE 参数可以是 'cpu、'memory 或者 'cpu+mem

之后我们可以执行导致卡顿的函数或者操作。

接着调用 profilter-report 停止采样并弹出一个类似火焰图/调用树的剖析报告缓冲区。

分析完毕之后可以调用 profiler-stop 停止Emacs内部的采样,避免持续开销。

我们以一个例子来说明如何解读数据

emacs-lisp 复制代码
(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耗时、一个显示内存,它们的内容如下:

txt 复制代码
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

emacs-lisp 复制代码
(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))

执行之后我们可以发现调用结果如下::

txt 复制代码
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 函数,它可以按照需求多次调用我们的函数,并输出调用结果。它的签名如下:

emacs-lisp 复制代码
(benchmark-run &optional REPETITIONS &rest FORMS)

REPETITIONS 是一个数字表示应该运行后面的 FORMS 多少次。输出的结果是一个形如 (总耗时(秒) GC执行次数 GC总耗时(秒)) 的列表

我们可以分别执行下面的测试代码

emacs-lisp 复制代码
(benchmark-run 1 (my-inefficient-string-join 1000))
(benchmark-run 1 (my-inefficient-string-join 2000))
(benchmark-run 1 (my-inefficient-string-join 4000))

结果如下:

txt 复制代码
(0.057176 2 0.05415199999999931)
(0.159746 8 0.15689200000000003)
(0.649431 36 0.6428240000000001)

我们发现主要开销在GC上,调用次数是2倍增长,但是GC次数大概是 4倍增长符合我们上面提到的 O(n²) 的时间复杂度。所以对于这种情况我们不应该使用 concat 来连接。

其实对于这种需要频繁插入新元素的场景,正确的做法是使用 list 作为基础数据结构。我们的上述代码可以改为:

emacs-lisp 复制代码
(defun my-efficient-string-join (n)
  (let ((lst '()))
    (dotimes (i n)
      (push (number-to-string i) lst))
    (string-join (nreverse lst) "")))

最终再次执行 benchmark-run 得到的结果如下:

emacs-lisp 复制代码
(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 单独多次执行表达式,获取性能信息

相关推荐
aluluka3 天前
《elisp 每日一练》——定时器与进程调用综合练习
emacs·lisp·elisp
aluluka4 天前
《elisp 每日一练》——Process Sentinel 与 json集成
json·emacs·lisp·elisp
Masimaro7 天前
《elisp 每日一练》—— 同步进程调用
emacs
aluluka8 天前
《elisp 每日一练》——定时器机制
emacs·lisp·elisp
aluluka9 天前
《elisp 每日一练》——hook 与 advice 综合练习
emacs·lisp·elisp
Masimaro12 天前
《elisp 每日一练》——mode hook
emacs
智购科技自动售货机工厂12 天前
省电40%的底层逻辑:冷热双温售货机基于SaaS的智能温控算法~YH
单片机·嵌入式硬件·百度·架构·emacs
Masimaro15 天前
《elisp》 每日一练——hook 机制详解
emacs
Masimaro15 天前
《elisp 每日一练》——major mode 综合练习
emacs