Optimization Common Lisp:优化文件解析以减少读取和内存分配

Optimization Common Lisp:优化文件解析以减少读取和内存分配,optimization,common-lisp,Optimization,Common Lisp,我需要一些帮助来优化一些常见的Lisp代码。我正试图从日志文件中查询数据。从超过14.5公里的线路中拔出前50条线路需要一秒钟的时间。推断出来,仅仅从日志文件中读取数据几乎需要5分钟。另外,我当前实现的前50行分配了~50MB,而整个文件只有14MB。我要做的是执行1次数据读取,以最小数量的内存分配解析数据 我知道我看到的性能下降是由于我的代码。我现在很难集中精力思考的是如何重构我的代码以最小化我所看到的问题。我曾尝试使用WITH-INPUT-FROM-string以流的形式访问字符串,但性能没

我需要一些帮助来优化一些常见的Lisp代码。我正试图从日志文件中查询数据。从超过14.5公里的线路中拔出前50条线路需要一秒钟的时间。推断出来,仅仅从日志文件中读取数据几乎需要5分钟。另外,我当前实现的前50行分配了~50MB,而整个文件只有14MB。我要做的是执行1次数据读取,以最小数量的内存分配解析数据

我知道我看到的性能下降是由于我的代码。我现在很难集中精力思考的是如何重构我的代码以最小化我所看到的问题。我曾尝试使用WITH-INPUT-FROM-string以流的形式访问字符串,但性能没有明显变化

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

使用我的代码:使用8个可用的CPU内核运行需要1138000微秒(1.138000秒)。 在此期间,在用户模式下花费了1138807微秒(1.138807秒) 在系统模式下花费了0微秒(0.000000秒) 在GC中花费19004微秒(0.019004秒)。 分配的内存为49249040字节

(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)))))
(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)))))
如果没有我的代码:使用8个可用CPU内核运行需要64000微秒(0.064000秒)。 在此期间,在用户模式下花费了62401微秒(0.062401秒) 在系统模式下花费了0微秒(0.000000秒) 已分配834512字节内存

(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)))))
(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)))))

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

问题在于慢性软件包,它在内部使用本地时间软件包

这:

正在压垮你

chronicity:month of
调用
本地时间:timestamp month
。如果你看一下代码:

 (nth-value 1
         (%timestamp-decode-date
          (nth-value 1 (%adjust-to-timezone timestamp timezone))))
所以,这里是解码基本日期(看起来是一个整数),两次(一次是时区,一次是月份)

所以你在解码同一个日期,做同样的工作,每个日期6次。这些例行程序正在酝酿一场风暴

你也给subseq打了两次电话

因此,在我看来,您需要将精力集中在日期解析逻辑上。在本例中,使用一些不太通用的方法。您不必验证日期(假定日志是准确的),并且您不需要从epoch开始转换为天/秒/毫秒,您只需要单个的MDY、HMS数据。您正在使用当前包完成所有这些工作,一旦您创建了通用时间,它是多余的

你也可能不在乎时区


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

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

(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-*
函数的重复位。

我对代码进行了一些修改,并大大减少了执行所需的时间和分配的字节数

新代码:运行时间为65000微秒(0.065000秒) 具有8个可用的CPU内核。 在此期间,在用户模式下花费了62400微秒(0.062400秒) 在系统模式下花费了0微秒(0.000000秒) 在GC中花费了2001微秒(0.002001秒)。 分配的内存为1029024字节

(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)))))
(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流库或它的一个依赖项有关

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

如果不进行解析,只在未压缩文件的相同10k行上执行读取操作,大约需要十分之一秒,并且只分配28MB

现在我对gzip流的性能无能为力,所以我不得不忍受它,直到性能不再可以忍受为止


感谢大家的帮助和建议。

问题并不在于chronicity软件包本身,而是在于使用它的访问器来创建世界时间。只需使用
(本地时间:时间戳到世界日期时间)