【问题标题】:Why does calling code as a function take longer than calling it directly in Clozure Common lisp?为什么将代码作为函数调用比在 Clozure Common lisp 中直接调用需要更长的时间?
【发布时间】:2019-10-18 15:19:33
【问题描述】:

我有大约 900000 条记录:

(defparameter RECORDS 
  '((293847 "john menk" "john.menk@example.com" 0123456789 2300 2760 "CHEQUE" 012345 "menk freeway" "high rose")
    (244841 "january agami" "j.a@example.com" 0123456789 2300 2760 "CHEQUE" 012345 "ishikawa street" "fremont apartments")
    ...))

(这些是从文件中读取的。上面的代码仅作为示例提供。它有助于显示这些数据的内部结构。)

为了快速制作原型,我为选择器使用别名:

(defmacro alias (new-name existing-name)

  "Alias NEW-NAME to EXISTING-NAME. EXISTING-NAME has to be a function."

  `(setf (fdefinition ',new-name) #',existing-name))

(progn 
  (alias account-number first)
  (alias full-name second)
  (alias email third)
  (alias mobile fourth)
  (alias average-paid fifth)
  (alias highest-paid sixth)
  (alias usual-payment-mode seventh)
  (alias pincode eighth)
  (alias road ninth)
  (alias building tenth))

现在我运行:

(time (loop for field in '(full-name email) 
        append (loop for record in RECORDS
                 when (cl-ppcre:scan ".*?january.*?agami.*?" 
                                     (funcall (symbol-function field) record))
                 collect record)))

repl 输出:

...
took 1,714 milliseconds (1.714 seconds) to run.
During that period, and with 4 available CPU cores,
     1,698 milliseconds (1.698 seconds) were spent in user mode
         9 milliseconds (0.009 seconds) were spent in system mode
 40 bytes of memory allocated.
...

定义一个做同样事情的函数:

(defun searchx (regex &rest fields) 
  (loop for field in fields 
    append (loop for record in RECORDS
             when (cl-ppcre:scan regex (funcall (symbol-function field) record))
             collect record)))

然后调用它:

(time (searchx ".*?january.*?agami.*?" 'full-name 'email))

输出:

...
took 123,389 milliseconds (123.389 seconds) to run.
         992 milliseconds (  0.992 seconds, 0.80%) of which was spent in GC.
During that period, and with 4 available CPU cores,
     118,732 milliseconds (118.732 seconds) were spent in user mode
       4,569 milliseconds (  4.569 seconds) were spent in system mode
 2,970,867,648 bytes of memory allocated.
 501 minor page faults, 0 major page faults, 0 swaps.
...

几乎慢了 70 倍?!!

我认为这可能是特定于计算机的问题。所以我在两台不同的机器上运行了相同的代码。一台 macbook air 和一台 macbook pro。个人时间不同,但行为是一致的。将它作为函数调用比直接在两台机器上调用它花费的时间要长得多。当然,单个函数调用的开销应该不会那么慢。


然后我认为可能是 Clozure CL 负责。所以我在 SBCL 中运行了相同的代码,即使在那里行为也很相似。差别不是很大,但还是很大的。它大约慢了 22 倍。

直接运行时SBCL输出:

Evaluation took:
  1.519 seconds of real time
  1.477893 seconds of total run time (0.996071 user, 0.481822 system)
  97.30% CPU
  12 lambdas converted
  2,583,290,520 processor cycles
  492,536 bytes consed

作为函数运行时的SBCL输出:

Evaluation took:
  33.522 seconds of real time
  33.472137 seconds of total run time (33.145166 user, 0.326971 system)
  [ Run times consist of 0.254 seconds GC time, and 33.219 seconds non-GC time. ]
  99.85% CPU
  56,989,918,442 processor cycles
  2,999,581,336 bytes consed

为什么将代码作为函数调用这么慢?我该如何解决?

【问题讨论】:

    标签: regex performance common-lisp


    【解决方案1】:

    差异可能是由于正则表达式。

    这里的正则表达式是一个文字字符串:

     (cl-ppcre:scan ".*?january.*?agami.*?" 
                    (funcall (symbol-function field) record))
    

    cl-ppcre:scan 函数有一个编译器宏来检测这种情况并生成一个(load-time-value (create-scanner ...)) 表达式(因为字符串不可能依赖于运行时值,这是可以接受的)。

    编译器宏也可能应用在您的测试中,在这种情况下,加载时间值可能只执行一次。

    然而,在以下代码中,正则表达式是一个运行时值,作为函数的输入获得:

    (defun searchx (regex &rest fields) 
      (loop for field in fields 
        append (loop for record in RECORDS
                 when (cl-ppcre:scan regex (funcall (symbol-function field) record))
                 collect record)))
    

    在这种情况下,扫描器对象是在评估 scan 时构建的,即每次循环遍历记录时。

    为了检验这个假设,您可能需要执行以下操作:

    (defun searchx (regex &rest fields) 
      (loop 
        with scanner = (cl-ppcre:create-scanner regex)
        for field in fields 
        append (loop for record in RECORDS
                 when (cl-ppcre:scan scanner (funcall (symbol-function field) record))
                 collect record)))
    

    或者,不要更改功能,而是给它一个扫描仪:

    (time (searchx (cl-ppcre:create-scanner 
                     ".*?january.*?agami.*?")
                  'full-name 
                  'email))
    

    【讨论】:

    • 修改后的 searchx 有效。现在函数代码和直接代码的性能相似。我很好奇你是如何发现 cl:ppcre 有一个编译器宏来优化文字大小写的。你读过源代码吗?
    • 这在此处记录:edicl.github.io/cl-ppcre/#blabla 但至少在 SBCL 中,如果您检查符号 ppcre:scan,它会告诉您它有一个编译器宏 iirc
    猜你喜欢
    • 1970-01-01
    • 2017-10-13
    • 2013-08-08
    • 2021-10-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多