无差别地循环数组或列表

Tho*_*ier 5 sbcl common-lisp

问题

假设您有多个列表或数组,为了示例,我们假设有两个:

(defparameter *arr* #(1 2 3))
(defparameter *list* '(4 5 6))
Run Code Online (Sandbox Code Playgroud)

您可以使用或关键字loop来覆盖它们:acrossin

(loop for elem across *arr* do (format t "~a" elem))
     => 123
(loop for elem in *list* do (format t "~a" elem))
     => 456
Run Code Online (Sandbox Code Playgroud)

我希望能够loop使用相同的语法来遍历这些数组或列表。我正在使用 SBCL,执行速度是一个问题。

使用being the elements of

这种语法很好,因为无论其参数是 alist或 ,它都可以工作array

(loop for elem being the elements of *arr* do (format t "~a" elem))
     => 123
(loop for elem being the elements of *list* do (format t "~a" elem))
     => 456
Run Code Online (Sandbox Code Playgroud)

但其速度却十分恐怖。如果我们通过访问 100 个元素的列表或数组 1M 次来进行快速比较:

(format t "# Test 1.1.1 : Accessing list of doubles with loop 'in': ") (terpri)
(let ((test-list (make-list 100 :initial-element 12.2d0))
      (testvar 0d0))
  (declare (optimize (speed 3))
           (type list test-list)
           (type double-float testvar))
  (time (dotimes (it 1000000 t) (loop for el in test-list do
                                      (setf testvar (the double-float el))))))

(format t "# Test 1.1.2 : Accessing list of doubles with loop 'elements': ") (terpri)
(let ((test-list (make-list 100 :initial-element 12.2d0))
      (testvar 0d0))
  (declare (optimize (speed 3))
           (type list test-list)
           (type double-float testvar))
  (time (dotimes (it 1000000 t) (loop for el being the elements of test-list do
                                      (setf testvar (the double-float el))))))

(format t "# Test 1.2.1 : Accessing simple-array of doubles using loop 'across' : ") (terpri)
(let ((test-array (make-array 100 :initial-element 12.2d0 :element-type 'double-float))
      (testvar 0d0))
  (declare (optimize (speed 3))
           (type double-float testvar)
           (type simple-array test-array))
  (time (dotimes (it 1000000 t) (loop for el across test-array do
                                      (setf testvar (the double-float el))))))

(format t "# Test 1.2.2 : Accessing simple-array of doubles using loop 'elements' : ") (terpri)
(let ((test-array (make-array 100 :initial-element 12.2d0 :element-type 'double-float))
      (testvar 0d0))
  (declare (optimize (speed 3))
           (type double-float testvar)
           (type simple-array test-array))
  (time (dotimes (it 1000000 t) (loop for el being the elements of test-array do
                                      (setf testvar (the double-float el))))))
Run Code Online (Sandbox Code Playgroud)

它给我们:

# Test 1.1.1 : Accessing list of doubles with loop 'in': 
Evaluation took:
  0.124 seconds of real time
  0.123487 seconds of total run time (0.123471 user, 0.000016 system)
  99.19% CPU
  445,008,960 processor cycles
  672 bytes consed
  
# Test 1.1.2 : Accessing list of doubles with loop 'elements': 
Evaluation took:
  0.843 seconds of real time
  0.841639 seconds of total run time (0.841639 user, 0.000000 system)
  99.88% CPU
  3,034,104,192 processor cycles
  0 bytes consed
  
# Test 1.2.1 : Accessing simple-array of doubles using loop 'across' : 
Evaluation took:
  0.062 seconds of real time
  0.062384 seconds of total run time (0.062384 user, 0.000000 system)
  100.00% CPU
  224,896,032 processor cycles
  0 bytes consed
  
# Test 1.2.2 : Accessing simple-array of doubles using loop 'elements' : 
Evaluation took:
  1.555 seconds of real time
  1.554472 seconds of total run time (1.541572 user, 0.012900 system)
  [ Run times consist of 0.094 seconds GC time, and 1.461 seconds non-GC time. ]
  99.94% CPU
  5,598,161,100 processor cycles
  1,600,032,848 bytes consed
Run Code Online (Sandbox Code Playgroud)

我认为它必须使用elt访问器?无论如何,速度上的惩罚是不可接受的。

尝试巧妙地使用宏

我写了一些东西来实现我的目标,即list和具有相同的语法array。我认为这不太好,因为它看起来过于尴尬,但这里:

(defun forbuild (el-sym list-or-array list-or-array-sym)
  "Outputs either :
     * (for el-sym in list-or-array)
     * (for el-sym across list-or-array)
Depending on type of list-or-array.
el-sym : symbol, eg. 'it1
list-or-array : declared, actual data for list or array
list-or-array-sym : symbol name for the table, to avoid writing the data in full
                    in the 'loop' call using eval.
Example call : (forbuild 'it1 arr 'arr)"
  (cond ((typep list-or-array 'array)
         `(for ,el-sym across ,list-or-array-sym))
        ((typep list-or-array 'list)
         `(for ,el-sym in ,list-or-array-sym))))

(defun forbuild-l (l-elsyms l-lars l-larsyms)
  "forbuild but over lists of things."
  (let ((for-list nil)
        (list-temp nil))
    (loop for elem in l-elsyms
          for lar in l-lars
          for larsym in l-larsyms do
          (setf list-temp (forbuild elem lar larsym))
          (loop for word-temp in list-temp do
                (push word-temp for-list)))
    (nreverse for-list)))

(defun loop-expr (forlist body)
  "Creates the expression ready to be evaluated to execute the loop.
forlist : List of symbols to be inserted syntactically. eg.
          FOR IT1 ACROSS ARR1 FOR IT2 IN ARR2
body : all the expression after the 'for' clauses in the 'loop'."
  `(loop ,@forlist ,@body))

(defmacro looparl (element list-or-array &rest body)
  (let ((forlist (gensym)))
    `(let ((,forlist (forbuild2-l (quote ,element)
                                  (list ,@list-or-array)
                                  (quote ,list-or-array))))
       (loop-expr ,forlist (quote ,body)))))
Run Code Online (Sandbox Code Playgroud)

基本上我loop根据参数构建正确的语法。这里给出的版本looparl可以这样调用:

(let ((arr1 #(7 8 9))
      (list2 (list 10 11 12)))
    (looparl (it1 it2) (arr1 list2) do (format t "~a ~a" it1 it2) (terpri)))
=> (LOOP FOR IT1 ACROSS ARR1
  FOR IT2 IN LIST2
  DO (FORMAT T "~a ~a" IT1 IT2) (TERPRI))
Run Code Online (Sandbox Code Playgroud)

本示例中省略了此输出表达式的实际计算,因为它不适用于非全局名称。如果我们在末尾添加一个 eval looparl

(defmacro looparl (element list-or-array &rest body)
  (let ((forlist (gensym)))
    `(let ((,forlist (forbuild2-l (quote ,element)
                                  (list ,@list-or-array)
                                  (quote ,list-or-array))))
       (eval (loop-expr ,forlist (quote ,body))))))
Run Code Online (Sandbox Code Playgroud)

在处理全局变量时,我们发现仍然存在速度问题,因为在运行时会发生评估:

(looparl (it1 it2) (*arr* *list*) for it from 100
              do (format t "~a ~a ~a" it1 it2 it) (terpri))
=> 1 4 100
   2 5 101
   3 6 102
(time (dotimes (iter 1000 t) (looparl (it1 it2) (*arr* *list*) for it from 100
              do (format t "~a ~a ~a" it1 it2 it) (terpri))))
=> Evaluation took:
  1.971 seconds of real time
  1.932610 seconds of total run time (1.892329 user, 0.040281 system)
  [ Run times consist of 0.097 seconds GC time, and 1.836 seconds non-GC time. ]
  98.07% CPU
  1,000 forms interpreted
  16,000 lambdas converted
  7,096,353,696 processor cycles
  796,545,680 bytes consed
Run Code Online (Sandbox Code Playgroud)

每个宏一次都会被评估一千次。但肯定类型在编译时是已知的不是吗?中的语法类型looparl非常好,我希望能够在不影响速度的情况下使用它。

我在Peter Seibel 的书 Practical Common Lisp 的“Loop for Black Belts”一章中读到了这个注释

3 您可能想知道为什么 LOOP 不需要不同的介词就无法确定它是在列表还是向量上循环。这是 LOOP 作为宏的另一个结果:列表或向量的值直到运行时才知道,但 LOOP 作为宏,必须在编译时生成代码。LOOP 的设计者希望它能够生成极其高效的代码。为了能够生成有效的代码来循环遍历一个向量,它需要在编译时知道该值在运行时将是一个向量——因此,需要不同的介词。

我是否犯了一些大的 Common-Lisp 废话?您将如何快速创建一个工作looparl

编辑1:FOR图书馆

FOR感谢 Ehvince 对图书馆的参考。over函数中的关键字确实for:for正是我所需要的。然而,基准测试确实令人印象深刻:

(let ((test-list (make-list 100 :initial-element 12.2d0))
      (testvar 0d0))
  (declare (optimize (speed 3))
           (type list test-list)
           (type double-float testvar))
  (time (dotimes (it 1000000 t) 
          (for:for ((el over test-list))
            (setf testvar (the double-float el))))))

(let ((test-array (make-array 100 :initial-element 12.2d0))
      (testvar 0d0))
  (declare (optimize (speed 3))
           (type simple-array test-array)
           (type double-float testvar))
  (time (dotimes (it 1000000 t) 
          (for:for ((el over test-array))
            (setf testvar (the double-float el))))))

Evaluation took:
  4.802 seconds of real time
  4.794485 seconds of total run time (4.792492 user, 0.001993 system)
  [ Run times consist of 0.010 seconds GC time, and 4.785 seconds non-GC time. ]
  99.83% CPU
  17,286,934,536 processor cycles
  112,017,328 bytes consed
  
Evaluation took:
  6.758 seconds of real time
  6.747879 seconds of total run time (6.747879 user, 0.000000 system)
  [ Run times consist of 0.004 seconds GC time, and 6.744 seconds non-GC time. ]
  99.85% CPU
  24,329,311,848 processor cycles
  63,995,808 bytes consed
Run Code Online (Sandbox Code Playgroud)

该库使用专用关键字的速度inacross标准的速度相同loop。但非常慢over

编辑2:mapetypecase

感谢 sds 和 Rainer Joswig 的建议。它确实适用于我只有一个数组/列表进行迭代的简单情况。让我告诉你我想到的一个用例:我正在实现一个gnuplot包装器,既作为培训,又在我的工具包中拥有我自己的程序。我想从用户列表或数组中获取无差别的数据作为系列来通过管道传输到 gnuplot。这就是为什么我需要能够同时循环多个数组/列表+使用优雅的循环子句来获取迭代次数等。

在这个用例(gnuplot包装器)中,我的每个“数据块”中只有两个或三个for子句loop,所以我想到根据手动输入的类型编写每个组合,这是可能的,但非常尴尬。如果我必须做类似的事情,我就会被困住:

(loop for el1 in list1
      for el2 across arr1
      for el3 in list2
      for el4 in list3
      ...)
Run Code Online (Sandbox Code Playgroud)

随着list-iarr-i被输入。此用例的另一个后备计划是将所有内容都转换为数组。

我认为,由于它很容易概念化,我可以一劳永逸地编写一些灵活且快速的东西,但一定有一个原因为什么它既不在规范中也不在 SBCL 特定的代码中。

sds*_*sds 3

您正在寻找的称为 map:两者

(map nil #'princ '(1 2 3))
Run Code Online (Sandbox Code Playgroud)

(map nil #'princ #(1 2 3))
Run Code Online (Sandbox Code Playgroud)

打印123

然而,列表和数组是非常不同的东西,最好提前决定要使用哪一种。