您有一个在编译时已知的键值对列表。我们不想在运行时查找,而是希望生成一个查找表。例如,我们可以这样做(来自我昨天写的一段代码):
(defun mod-mask (char)
(case char
(#\A #.(ash 1 22))
(#\s #.(ash 1 23))
(#\H #.(ash 1 24))
(#\S #.(ash 1 25))
(#\C #.(ash 1 26))
(#\M #.(ash 1 27))))
(defun mask-mod (mask)
(case mask
(#.(ash 1 22) #\A)
(#.(ash 1 23) #\s)
(#.(ash 1 24) #\H)
(#.(ash 1 25) #\S)
(#.(ash 1 26) #\C)
(#.(ash 1 27) #\M)))
但是你不想重复自己,而每个人都明白它的作用的整洁、简单的代码真的很无聊!
开个玩笑,上面的例子很短,但是可以有很多选择,而且数量非常多。我们不想输入太多内容,因为这样很容易出错而且很乏味。此外,列表还具有在其他地方查找的优点:例如,我们可以看到定义了哪些元素,而我们无法从编译的案例形式中获取哪些元素。与手动输入 case 语句不同,列表以及代码也可以在运行时生成。
我们制作一个列表来代替上面的:
(defvar *modifier-masks*
`((#\A . ,(ash 1 22))
(#\s . ,(ash 1 23))
(#\H . ,(ash 1 24))
(#\S . ,(ash 1 25))
(#\C . ,(ash 1 26))
(#\M . ,(ash 1 27))))
并创建一个宏来生成以上两种情况:
(defmacro mod-lookup-gen (&optional bit)
`(lambda (mask)
(case mask
,@(mapcar (lambda (e)
(if bit
`(,(car e) ,(cdr e))
`(,(cdr e) ,(car e))))
*modifier-masks*))))
我们可以在简单的包装中使用它:
(defun mask-mod (mask)
(let ((get-char (mod-lookup-gen)))
(funcall get-char mask)))
(defun mod-mask (modifier)
(let ((get-char (mod-lookup-gen 'bit)))
(funcall get-char modifier)))
问题:什么是更好的选择,以及更高效的编译时生成代码,相当于上面的两个 defun?有没有办法摆脱 lambda 包装 case 语句以跳过额外的函数调用?除了可能内联它之外?
一般来说,在 Common Lisp 中查找键值对最有效的方法是什么?由于编译器可以在编译时布置用于查找的静态代码,因此“switch”表达式是查找值的最快方法吗?我想这取决于实施,但也许可以提供一些一般性指导?
还要注意,hashmap 和 alist 之间的区别在于,alist 以及由此生成的 switch 语句关心语句的顺序,即它们按照声明的顺序进行匹配,而 hashmap 是无序查找。这可能重要也可能不重要,具体取决于用例。
顺便说明一下,这是“枚举”惯用语。我见过一些用于生成枚举的 CL 库,但我没有使用过任何库,所以我不知道它们生成什么样的代码。也许有人可以推荐一个可以生成良好的编译时查找的工具?
这个问题的答案是:编写代码并测量它。 (也许)很明显,在很多键的限制情况下,基于哈希表的东西会比基于
case
的东西更快。 但断点在哪里取决于实现的细节。 当然,如果愿意的话,足够激进的编译器总是可以将 case
变成哈希表。
对于您的示例,我编写了下面的代码,我发现,在一种实现中,
case
实现比哈希表快几倍,而 alist 则介于两者之间。 在另一种实现中,case
实现速度最快,哈希表速度较慢,alist 速度最慢。
如果您想了解有关性能的信息,则必须测量。
(eval-when (:compile-toplevel :load-toplevel :execute)
(defvar *modifier-masks*
`((#\A . ,(ash 1 22))
(#\s . ,(ash 1 23))
(#\H . ,(ash 1 24))
(#\S . ,(ash 1 25))
(#\C . ,(ash 1 26))
(#\M . ,(ash 1 27)))))
(declaim (inline modifier-case-map mask-case-map))
(defun modifier-case-map (c)
(declare (type character c))
(macrolet ((cmap (v)
`(ecase ,v
,@(mapcar (lambda (m)
`(,(car m) ,(cdr m)))
*modifier-masks*))))
(cmap c)))
(defun mask-case-map (m)
(declare (type fixnum m))
(macrolet ((mmap (v)
`(ecase ,v
,@(mapcar (lambda (m)
`(,(cdr m) ,(car m)))
*modifier-masks*))))
(mmap m)))
(declaim (inline modifier-hash-map mask-hash-map))
(defun modifier-hash-map (c)
(declare (type character c))
(or (gethash c (load-time-value (let ((h (make-hash-table
:size (length *modifier-masks*))))
(dolist (m *modifier-masks* h)
(setf (gethash (car m) h) (cdr m))))))
(error "not present")))
(defun mask-hash-map (m)
(declare (type fixnum m)) ;?
(or (gethash m (load-time-value (let ((h (make-hash-table
:size (length *modifier-masks*))))
(dolist (m *modifier-masks* h)
(setf (gethash (cdr m) h) (car m))))))
(error "not present")))
(declaim (inline modifier-alist-map mask-alist-map))
(defun modifier-alist-map (c)
(declare (type character c))
(or (cdr (assoc c (load-time-value *modifier-masks* t)))
(error "not present")))
(defun mask-alist-map (m)
(declare (type fixnum m)) ;?
(or (gethash m (load-time-value *modifier-masks* t))
(error "not present")))
;;; All the setfery is to try to make sure things do not get optimized
;;; away
(defun tsc (n)
(let ((r nil))
(dotimes (i n r)
(dolist (c (load-time-value (mapcar #'car *modifier-masks*)))
(setf r (modifier-case-map c))))))
(defun tsh (n)
(let ((r nil))
(dotimes (i n r)
(dolist (c (load-time-value (mapcar #'car *modifier-masks*)))
(setf r (modifier-hash-map c))))))
(defun tsa (n)
(let ((r nil))
(dotimes (i n r)
(dolist (c (load-time-value (mapcar #'car *modifier-masks*)))
(setf r (modifier-alist-map c))))))