科林·伍德伯里 - 优化 Common Lisp

最近发布了一个用于 Common Lisp 的Parser Combinator 库,但对其性能不满意。通过使用 SBCL 内置的sb-sprof,确定了 CPU 和内存分配热点,将parcom/json模块的运行时速度提高了 3 倍,内存分配减少了 25 倍。

  • sb-sprof 介绍:SBCL 有多个内置扩展库,sb-sprof是一个“统计分析器”,可定期采样调用栈以查看时间花费在哪里以及谁在进行最多的分配。在parcom/benchmarks系统中,通过(require :sb-sprof)引入。可通过(with-profiling :max-samples :sample-interval :report :mode)来交互式地分析函数性能,time可快速评估整体性能,with-profiling:report模式通常为:flat,设置为:graph可显示函数调用关系,:mode :alloc可使输出聚焦于内存。
  • 优化技术

    • 避免工作:最快的解决问题方法是消除它,对于parcom,应避免使用many而优先使用take-while,以减少列表分配。
    • simple-string 和 schar:在 Common Lisp 中,字符串是向量的一种,知道输入是simple-string可使用更快的schar访问元素,避免sb-sprof结果中出现大量%DATA-VECTOR-INDEX调用。
    • 类型签名:使用char-string类型声明可让编译器优化schar调用,明确数字输入和输出类型可避免内部调用GENERIC-+等函数,在loop变量中声明类型可避免通用调度。
    • 多返回值:Common Lisp 中(cons a b)调用会进行堆分配,在parcom中使用valuesmultiple-value-bind可避免此成本,减少内存使用。
    • 栈分配:通过(declare (dynamic-extent work))可让编译器在栈上分配局部变量,提高性能并减少内存使用。
    • Lambda 缓存:Common Lisp 每次调用 lambda 都要进行分配,通过自定义“lambda 缓存”,使用make-hash-tablegethash来缓存已分配的 lambda,可减少内存分配。
  • 结论:可使用timesb-sprof:with-profiling确定代码的时间和内存分配热点,从而提高代码性能,证明 Parser Combinators 在 Common Lisp 中是一种既优雅又高效的文本解析方式。
  • 反馈

    • fixnum 是否足够大parcom中所有字符串索引和循环变量都使用fixnum,在不同编译器中most-positive-fixnum的值不同,对于parcom在内存中的字符串操作,通常足够大,除非要解析大于 2gb 的 JSON 文件。
    • (safety 0) 还是 (safety 1):在 SBCL 中,将(safety 0)改为(safety 1)后,ECL 的性能有所下降,其他编译器变化不大,作者仍使用(safety 0),仅在特定位置使用。
阅读 8
0 条评论