最近我有一个处理大文件的任务,文件大小为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