最近我有一个处理大文件的任务,文件大小为460MB,并包含5777672行.当我使用 linux内置命令’wc’来计算文件行号时,它非常快: time wc -l large_ess_test.log5777672 large_ess_test.logreal 0m0.144suser 0m0.
time wc -l large_ess_test.log 5777672 large_ess_test.log real 0m0.144s user 0m0.052s sys 0m0.084s
然后我使用以下代码来计算Common Lisp中的行号(SBCL 1.3.7 64位)
#!/usr/local/bin/sbcl --script
(defparameter filename (second *posix-argv*))
(format t "nline: ~D~%"
(with-open-file (in filename)
(loop for l = (read-line in nil nil)
while l
count l)))
结果让我很失望,因为与’wc’命令相比它真的很慢.我们只计算行号,即使没有任何其他操作:
time ./test.lisp large_ess_test.log nline: 5777672 real 0m3.994s user 0m3.808s sys 0m0.152s
我知道SBCL提供了C函数接口,我们可以直接调用C程序.我相信如果我直接调用C函数,性能会提高,所以我写下面的代码:
#!/usr/local/bin/sbcl --script
(define-alien-type pointer (* char))
(define-alien-type size_t unsigned-long)
(define-alien-type ssize_t long)
(define-alien-type FILE* pointer)
(define-alien-routine fopen FILE*
(filename c-string)
(modes c-string))
(define-alien-routine fclose int
(stream FILE*))
(define-alien-routine getline ssize_t
(lineptr (* (* char)))
(n (* size_t))
(stream FILE*))
;; The key to improve the performance:
(declaim (inline getline))
(declaim (inline read-a-line))
(defparameter filename (second *posix-argv*))
(defun read-a-line (fp)
(with-alien ((lineptr (* char))
(size size_t))
(setf size 0)
(prog1
(getline (addr lineptr) (addr size) fp)
(free-alien lineptr))))
(format t "nline: ~D~%"
(let ((fp (fopen filename "r"))
(nline 0))
(unwind-protect
(loop
(if (= -1 (read-a-line fp))
(return)
(incf nline)))
(unless (null-alien fp)
(fclose fp)))
nline))
注意有两个’declaim’行.如果我们不写这两行,性能几乎与以前的版本相同:
;; Before declaim inline: ;; time ./test2.lisp large_ess_test.log ;; nline: 5777672 ;; real 0m3.774s ;; user 0m3.604s ;; sys 0m0.148s
但如果我们写出这两行,性能就会大幅提升:
;; After delaim inline: ;; time ./test2.lisp large_ess_test.log ;; nline: 5777672 ;; real 0m0.767s ;; user 0m0.616s ;; sys 0m0.136s
我认为第一个版本的性能问题是’read-line’除了从流中读取一行之外还做了很多其他事情.此外,如果我们可以获得“读取线”的内联版本,速度将会提高.问题是我们可以这样做吗?是否还有其他(标准)方法可以在不依赖FFI(非标准)的情况下提高读取性能?
READ-LINE的主要问题之一是它为每个调用分配一个新字符串.这可能需要花费时间,具体取决于实施方案.Common Lisp标准缺少一个函数,它将一行读入字符串缓冲区.
一些实现提供了将行读入缓冲区的函数的解决方案.例如,Allegro CL中的功能READ-LINE-INTO.
通常,实现提供缓冲输入的流.可以在此基础上实现搜索换行符,但是其代码可能是特定于实现的(或使用一些流抽象)和/或复杂的.
我不知道是否有这样的功能的官方实现,但这里可以找到类似的东西 – 对于SBCL来说看起来很复杂:
https://github.com/ExaScience/elprep/blob/master/buffer.lisp中的read-line-into-buffer
