Common Lisp:优化文件解析以实现最小读取和内存分配

时间:2013-10-19 21:08:43

标签: optimization common-lisp

我可以使用一些帮助来优化一些Common Lisp代码。我试图从日志文件中查询数据。从超过14.5k行中拉出前50行需要一秒钟。推断出来,只需要5分钟就能从日志文件中读取数据。另外,当我的当前实现的前50行分配大约50MB,而整个文件只有14MB。我想要的地方是执行1次读取数据,用最少的内存分配来解析它。

我知道我看到的性能打击是由于我的代码。我正在缠绕我的大脑的困难是如何重构我的代码,以尽量减少我所看到的问题。我尝试使用WITH-INPUT-FROM-STRING将字符串作为流访问,并且性能没有明显改变。

这是一个IIS日志,因此它将具有一致的结构。前两个字段是日期和时间,我想将其解析为一个数字,这样我就可以在需要时限制数据范围。之后,大多数字段的大小都是可变的,但所有字段都用空格分隔。

使用My Code:使用8个可用CPU核心运行1,138,000微秒(1.138000秒)。 在此期间,用户模式花费了1,138,807微秒(1.138807秒)                     在系统模式下花费了0微秒(0.000000秒) 在GC中花费了19,004微秒(0.019004秒)。  分配了49,249,040字节的内存。

没有我的代码:花了64,000微秒(0.064000秒)来运行8个可用的CPU核心。 在此期间,在用户模式下花费了62,401微秒(0.062401秒)                     在系统模式下花费了0微秒(0.000000秒)  分配了834,512个字节的内存。

(defun read-date-time (hit)
  (let ((date-time (chronicity:parse (subseq hit 0 20))))
    (encode-universal-time (chronicity:sec-of date-time)
               (chronicity:minute-of date-time)
               (chronicity:hour-of date-time)
               (chronicity:day-of date-time)
               (chronicity:month-of date-time)
               (chronicity:year-of date-time))))

(defun parse-hit (hit)
  (unless (eq hit :eof)
    (cons (read-date-time hit)
          (split-sequence:split-sequence #\Space (subseq hit 20)))))


(time (gzip-stream:with-open-gzip-file (ins "C:\\temp\\test.log.gz") 
  (read-line ins nil :eof)
  (loop for i upto 50 
     do (parse-hit (read-line ins nil :eof)))))

我的第一次尝试是一种非常天真的方法,我认识到我的代码现在可以使用一些改进,所以我要求一些方向。如果教程是更适合回答此问题的方法,请发布链接。我喜欢

3 个答案:

答案 0 :(得分:3)

问题是Chronicity包,它在内部使用了Local Time包。

此:

   (encode-universal-time (chronicity:sec-of date-time)
           (chronicity:minute-of date-time)
           (chronicity:hour-of date-time)
           (chronicity:day-of date-time)
           (chronicity:month-of date-time)
           (chronicity:year-of date-time))))

压垮你。

chronicity:month-of来电local-time:timestamp-month。如果你看一下那个代码:

 (nth-value 1
         (%timestamp-decode-date
          (nth-value 1 (%adjust-to-timezone timestamp timezone))))

所以,这是它解码基本日期(似乎是一个整数),两次,(一次是时区,一次是月份。

所以你要解码相同的日期,做同样的工作,每个日期6次。而那些惯例正在掀起风暴。

你也在两次调用subseq。

所以,在我看来,在这种情况下你需要专注于日期解析逻辑,使用不太通用的东西。您不必验证日期(日志被假定为准确),并且您不需要转换为自纪元以来的天/秒/毫秒,您只需要单独的MDY,HMS数据。你正在使用当前的软件包完成所有这些工作,一旦你创建了通用时间,它就是多余的。

你也可能不关心时区。

无论如何,这是问题的开始。它还不是I / O问题。

答案 1 :(得分:0)

这是我尝试过的例子,而不是一个完整的日志解析器,当然你可以选择以不同的方式表示数据/选择对你来说重要的东西,这只是一般的想法:

(defparameter *log*
  "2012-11-04 23:00:04 10.1.151.54 GET /_layouts/1033/js/global.js v=5 443 - 10.1.151.61 Mozilla/5.0+(Windows+NT+6.1)+AppleWebKit/535.2+(KHTML,+like+Gecko)+Chrome/15.0.864.0+Safari/535.2 200 0 0 31
2012-11-04 23:00:04 10.1.151.54 GET /_layouts/1033/styles/css/topnav.css v=1 443 - 10.1.151.61 Mozilla/5.0+(Windows+NT+6.1)+AppleWebKit/535.2+(KHTML,+like+Gecko)+Chrome/15.0.864.0+Safari/535.2 200 0 0 62
2012-11-04 23:00:07 10.1.151.54 GET /pages/index.aspx - 80 - 10.1.151.59 - 200 0 64 374
2012-11-04 23:00:07 10.1.151.52 GET /pages/index.aspx - 80 - 10.1.151.59 - 200 0 64 374")

(defstruct iis-log-entry
  year month day hour minute second ip method path)

(defstruct ipv4 byte-0 byte-1 byte-2 byte-3)

(defun parse-ipv4 (entry &key start end)
  (let* ((b0 (position #\. entry))
         (b1 (position #\. entry :start (1+ b0)))
         (b2 (position #\. entry :start (1+ b1))))
    (make-ipv4
     :byte-0 (parse-integer entry :start start :end b0)
     :byte-1 (parse-integer entry :start (1+ b0) :end b1)
     :byte-2 (parse-integer entry :start (1+ b1) :end b2)
     :byte-3 (parse-integer entry :start (1+ b2) :end end))))

(defun parse-method (entry &key start end)
  ;; this can be extended to make use of END argument and other
  ;; HTTP verbs
  (case (char entry start)
    (#\G 'get)
    (#\P (case (char entry (1+ start))
           (#\O 'post)
           (#\U 'put)))))

(defun parse-path (entry &key start) (subseq entry start))

(defun parse-iis-log-entry (entry)
  (let* ((ip-end (position #\Space entry :start 27))
         (method-end (position #\Space entry :start ip-end)))
    (make-iis-log-entry
     :year (parse-integer entry :start 0 :end 4)
     :month (parse-integer entry :start 5 :end 7)
     :day (parse-integer entry :start 8 :end 10)
     :hour (parse-integer entry :start 11 :end 13)
     :minute (parse-integer entry :start 14 :end 16)
     :second (parse-integer entry :start 17 :end 19)
     :ip (parse-ipv4 entry :start 20 :end ip-end)
     :method (parse-method entry :start (1+ ip-end) :end method-end)
     :path (parse-path entry :start method-end))))

(defun test ()
  (time
   (loop :repeat 50000 :do
      (with-input-from-string (stream *log*)
        (loop
           :for line := (read-line stream nil nil)
           :while line
           :for record := (parse-iis-log-entry line))))))

(test)
;; Evaluation took:
;;   0.790 seconds of real time
;;   0.794000 seconds of total run time (0.792000 user, 0.002000 system)
;;   [ Run times consist of 0.041 seconds GC time, and 0.753 seconds non-GC time. ]
;;   100.51% CPU
;;   1,733,036,566 processor cycles
;;   484,002,144 bytes consed

当然,您也可以使用一些宏来减少parse-*函数的重复位。

答案 2 :(得分:0)

我对代码进行了一些修改,缩短了执行时间和显着分配的字节数。

新代码:运行65,000微秒(0.065000秒)                     有8个可用的CPU核心。 在此期间,在用户模式下花费了62,400微秒(0.062400秒)                     在系统模式下花费了0微秒(0.000000秒) 在GC中花费了2,001微秒(0.002001秒)。  分配了1,029,024个字节的内存。

(let ((date-time-string (make-array 20 :initial-element nil)))
  (defun read-date-time (hit)
    (read-sequence date-time-string hit :start 0 :end 20)
    (local-time:timestamp-to-universal (local-time:parse-timestring (map 'string #'code-char date-time-string) :date-time-separator #\Space))))

(defun parse-hit (hit)
  (cons (read-date-time hit) (split-sequence:split-sequence #\Space (read-line hit))))

(defun timeparse (lines)
  (time (gzip-stream:with-open-gzip-file (ins "C:\\temp\\test.log.gz")
      (read-line ins nil :eof) 
      (loop for i upto lines
         do (parse-hit ins)))))

解压缩的文件是14MB,其中包含大约14.5k行。

解析文件的10k行会导致在8.5秒内分配178MB。我相信这与gzip-stream库或其依赖项之一有关。

无需解析,只需在压缩文件的10k行上执行读取操作需要8秒,并分配140MB内存。

如果没有解析,只需在未压缩文件的相同10k行上执行读取操作,大约需要1/10秒,并且只能分配28MB。

我现在对gzip-stream的性能没有太多了解,所以我必须忍受它,直到性能不再可以容忍。

感谢大家的帮助和建议。