可以在不使用Common Lisp中的eval的情况下执行此操作吗?

时间:2014-03-31 11:40:27

标签: common-lisp eval lambda processing-efficiency

在我的小项目中,我有两个数组,让我们称它们为A和B.它们的值是 #(1 2 3)#(5 6 7)。我还有两个相同长度的符号列表,我们称之为C和D.它们看起来像这样:(num1 num2 num3)(num2 num3 num4)

你可以说列表C和D中的符号是数组A和B中值的文本标签。因此A中的num1是1. A中的num2是2.B中的num2是5.没有num1在B中,但有一个num3,即6。

我的目标是生成一个带有两个参数的函数,如下所示:

(defun row-join-function-factory (C D)
...body...)

我希望它返回两个参数的函数:

(lambda (A B) ...body...)

这样使用参数A和B调用的结果函数会产生一种返回新数组的“join”:#(1 5 6 7)

在后面的函数中发生的过程从两个数组A和B获得值,这样它就产生一个新数组,其成员可以用(union C D)表示。注意:我实际上并没有运行(union C D),因为我实际上并不关心其中包含的符号的顺序,但我们假设它返回(num1 num2 num3 num4)。重要的是,(num1 num2 num3 num4)对应于新数组#(1 5 6 7)的文本标签。如果在C和D中都存在num2或任何符号,并且随后表示来自A和B的值,则对应于该符号的B中的值将保留在结果数组中,而不是来自A的值。

我希望在这里得到机械动作的要点。理论上,我希望row-join-function-factory能够使用任何长度/内容的数组和符号列表来执行此操作,但是编写这样的函数不仅仅是我,而不是问题。

问题是,我希望返回的函数非常高效,这意味着我不愿意让函数追逐指向列表,或者在运行时查找哈希表。在这个例子中,我需要返回的函数几乎是字面上的:

      (lambda (A B) 
(make-array 4 
    :initial-contents (list (aref A 0) (aref B 0) (aref B 1) (aref B 2))))

我不希望在运行时计算数组索引,或者它们引用的数组。我想要一个编译的函数来做到这一点,这只是尽可能快,尽可能少的工作。我不关心创建这样一个函数所需的运行时工作,只关心应用它所需的运行时工作。

我已经决定在row-join-function-factory中使用(eval )来处理代表上面的lisp代码的符号来生成这个函数。然而,我想知道,如果没有一些更简单的方法来解决这个我没想到的技巧,考虑到人们对使用eval的一般谨慎......

根据我的推理,我不能自己使用宏,因为他们无法知道A,B,C,D在编译时可以采用的所有值和维度,而我可以编写一个函数来返回一个机械的lambda做我想做的事情,我相信我的版本将总是做一些额外的运行时工作/关闭变量/等...与上面假设的lambda函数相比

欢迎提出想法,答案,建议等。我的结论是正确的,这是一种罕见的合法eval用途吗?我提前道歉,因为我无法用英语雄辩地表达这个问题......

(或者,如果有人可以解释我的推理在哪里,或者如何动态生成最有效的功能......)

4 个答案:

答案 0 :(得分:2)

据我了解,您需要预先计算矢量大小和aref参数。

(defun row-join-function-factory (C D)
  (flet ((add-indices (l n)
           (loop for el in l and i from 0 collect (list el n i))))
    (let* ((C-indices (add-indices C 0))
           (D-indices (add-indices D 1))
           (all-indices (append D-indices
                                (set-difference C-indices
                                                D-indices
                                                :key #'first)))
           (ns (mapcar #'second all-indices))
           (is (mapcar #'third all-indices))
           (size (length all-indices)))
      #'(lambda (A B)
          (map-into (make-array size)
                    #'(lambda (n i)
                        (aref (if (zerop n) A B) i))
                    ns is)))))

请注意,我使用了一个数字来了解是应该使用A还是B而不是捕获CD,以允许它们被垃圾收集。


编辑:我建议你对生成的函数进行分析,并观察运行时闭包的开销是否高于例如5%,针对特殊功能:

(defun row-join-function-factory (C D)
  (flet ((add-indices (l n)
           (loop for el in l and i from 0 collect (list el n i))))
    (let* ((C-indices (add-indices C 0))
           (D-indices (add-indices D 1))
           (all-indices (append D-indices
                                (set-difference C-indices
                                                D-indices
                                                :key #'first)))
           (ns (mapcar #'second all-indices))
           (is (mapcar #'third all-indices))
           (size (length all-indices))
           (j 0))
      (compile
       nil
       `(lambda (A B)
          (let ((result (make-array ,size)))
            ,@(mapcar #'(lambda (n i)
                          `(setf (aref result ,(1- (incf j)))
                                 (aref ,(if (zerop n) 'A 'B) ,i)))
                      ns is)
            result))))))

并验证编译开销是否确实在您的实现中得到回报。

我认为如果闭包和编译的lambda之间的运行时差异非常小,请保持闭包,对于:

  • 更清晰的编码风格
  • 根据实施情况,可能更容易调试
  • 根据实现,生成的闭包将共享功能代码(例如,闭包模板功能)
  • 在某些商业实现中,它不需要包含编译器的运行时许可证

答案 1 :(得分:1)

我认为正确的方法是使用一个可以在编译时计算索引的宏:

(defmacro my-array-generator (syms-a syms-b)
  (let ((table '((a 0) (b 0) (b 1) (b 2)))) ; compute this from syms-a and syms-b
    `(lambda (a b)
       (make-array ,(length table) :initial-contents
             (list ,@(mapcar (lambda (ai) (cons 'aref ai)) table))))))

它会产生你想要的东西:

(macroexpand '(my-array-generator ...))
==>
#'(LAMBDA (A B)
    (MAKE-ARRAY 4 :INITIAL-CONTENTS
                (LIST (AREF A 0) (AREF B 0) (AREF B 1) (AREF B 2))))

所以,剩下的就是写一个会产生

的函数
((a 0) (b 0) (b 1) (b 2))

给定的

syms-a = (num1 num2 num3) 

syms-b = (num2 num3 num4)

答案 2 :(得分:0)

取决于您何时知道数据。如果在编译时已知所有数据,则可以使用宏(根据sds的答案)。

如果数据在运行时已知,您应该考虑将其从现有阵列加载到2D阵列中。这 - 使用适当优化的编译器 - 应该意味着查找是几个muls,一个add和一个dereference。

顺便问一下,你能更详细地描述一下你的项目吗?听起来很有趣。 : - )

答案 3 :(得分:0)

鉴于CD,您可以创建一个类似

的闭包
(lambda (A B)
   (do ((result (make-array n))
        (i 0 (1+ i)))
       ((>= i n) result)
     (setf (aref result i)
           (aref (if (aref use-A i) A B)
                 (aref use-index i)))))

其中nuse-Ause-index是在闭包中捕获的预先计算值,如

n         --> 4
use-A     --> #(T nil nil nil)
use-index --> #(0 0 1 2)

使用SBCL(速度3)(安全0)检查执行时间基本上与make-array + initial-contents版本相同,至少对于这个简单的情况。

当然,使用这些预先计算的数据表创建一个闭包甚至不需要宏。

您是否实际计划使用展开的编译版本保存多少(如果有的话)?

修改

用SBCL进行实验,由

生成闭包
(defun merger (clist1 clist2)
  (let ((use1 (list))
        (index (list))
        (i1 0)
        (i2 0))
    (dolist (s1 clist1)
      (if (find s1 clist2)
          (progn
            (push NIL use1)
            (push (position s1 clist2) index))
          (progn
            (push T use1)
            (push i1 index)))
      (incf i1))
    (dolist (s2 clist2)
      (unless (find s2 clist1)
        (push NIL use1)
        (push i2 index))
      (incf i2))
    (let* ((n (length index))
           (u1 (make-array n :initial-contents (nreverse use1)))
           (ix (make-array n :initial-contents (nreverse index))))
      (declare (type simple-vector ix)
               (type simple-vector u1)
               (type fixnum n))
      (print (list u1 ix n))
      (lambda (a b)
        (declare (type simple-vector a)
                 (type simple-vector b))
        (let ((result (make-array n)))
          (dotimes (i n)
            (setf (aref result i)
                  (aref (if (aref u1 i) a b)
                        (aref ix i))))
          result)))))

运行速度比提供相同类型声明的手写版本慢约13%((a b c d)(b d e f)情况下的100,000,000次调用为2.878s而不是2.529s,为6个元素输出。)

基于数据的闭包版本的内部循环编译为

; 470: L2:   4D8B540801       MOV R10, [R8+RCX+1]   ; (aref u1 i)
; 475:       4C8BF7           MOV R14, RDI          ; b
; 478:       4C8BEE           MOV R13, RSI          ; source to use (a for now)
; 47B:       4981FA17001020   CMP R10, 537919511    ; (null R10)?
; 482:       4D0F44EE         CMOVEQ R13, R14       ; if true use b instead
; 486:       4D8B540901       MOV R10, [R9+RCX+1]   ; (aref ix i)
; 48B:       4B8B441501       MOV RAX, [R13+R10+1]  ; load (aref ?? i)
; 490:       4889440B01       MOV [RBX+RCX+1], RAX  ; store (aref result i)
; 495:       4883C108         ADD RCX, 8            ; (incf i)
; 499: L3:   4839D1           CMP RCX, RDX          ; done?
; 49C:       7CD2             JL L2                 ; no, loop back

条件不会编译为跳转,而是编译为条件赋值(CMOVEQ)。

我看到一点改进空间(例如直接使用CMOVEQ R13, RDI,保存指令并释放注册表)但我不认为这会削减13%。