我可以使用一些帮助来优化一些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)))))
我的第一次尝试是一种非常天真的方法,我认识到我的代码现在可以使用一些改进,所以我要求一些方向。如果教程是更适合回答此问题的方法,请发布链接。我喜欢
答案 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的性能没有太多了解,所以我必须忍受它,直到性能不再可以容忍。
感谢大家的帮助和建议。