最近发布了一个用于 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
中使用values
和multiple-value-bind
可避免此成本,减少内存使用。 - 栈分配:通过
(declare (dynamic-extent work))
可让编译器在栈上分配局部变量,提高性能并减少内存使用。 - Lambda 缓存:Common Lisp 每次调用 lambda 都要进行分配,通过自定义“lambda 缓存”,使用
make-hash-table
和gethash
来缓存已分配的 lambda,可减少内存分配。
- 避免工作:最快的解决问题方法是消除它,对于
- 结论:可使用
time
和sb-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)
,仅在特定位置使用。
- fixnum 是否足够大:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。