我有一个哈希表,其中键是相当复杂的列表,带有符号和整数的子列表,并且应根据已存在的值修改值。该表是使用:test #'equal
创建的。
我做了很多类似的事情:
(defun try-add (i)
(let ((old-i (gethash complex-list table nil)))
(if (may-add old-i)
(push i (gethash complex-list table)))))
分析表明equal
测试需要花费大量时间。我有一个优化的想法,gethash
查找量可以从两个减少到一个。它可以通过重用迭代器在C ++中完成,但不确定如何在Lisp中完成。有什么想法吗?
答案 0 :(得分:10)
不要做任何特别的事情,因为实施是为你做的。
当然,这种方法是特定于实现的,并且哈希表性能在实现之间有所不同。 (但随后优化问题始终是特定于实现的。)
以下答案适用于SBCL。我建议检查你的Lisp哈希表是否执行相同的优化。如果不是,请向您的供应商投诉!
SBCL中发生的事情是哈希表缓存GETHASH访问的最后一个表索引。
当调用PUTHASH(或等效地,(SETF GETHASH))时,它首先检查该缓存索引处的密钥是否是您传入的密钥的EQ。
如果是这样,则绕过整个哈希表查找例程,并且PUTHASH直接存储在缓存的索引中。
请注意,EQ只是一个指针比较,因此非常快 - 它根本不必遍历列表。
因此,在您的代码示例中,根本没有开销。
答案 1 :(得分:1)
您实际上可能正在访问哈希表三次。为什么?因为push
宏可以扩展为执行gethash
获取列表的代码,然后执行一些system::sethash
操作来存储值。
在此问题中,您正在检查地点的值,即列表。如果该列表满足某些谓词测试,则将某些内容推送到该位置。
可以通过创建捕获此语义的特殊用途运算符来攻击此问题:
(push-if <new-value> <predicate> <place>)
例如:
(push-if i #'may-add (gethash complex-list table))
此push-if
被定义为一个宏,它使用get-setf-expansion
表单参数上的<place>
函数来获取生成代码以访问该位置所需的部分。
生成的代码计算加载表单以从该位置获取旧值,然后将条件应用于旧值,如果成功,则在从{{1}获取的相应临时存储变量中准备新值并评估商店表单。
这是你在便携式Lisp中可以做的最好的,你可能会发现这仍然执行两个哈希操作,如上所述。 (在这种情况下,你希望哈希表本身有一个不错的缓存优化。但至少它只有两个操作。)
该方法将与内置的变异形式一样优化:get-setf-expansion
,incf
,push
等。我们的rotatef
将与内置的相同-INS。
如果它仍然很糟糕(执行两个哈希来更新哈希位置,没有缓存优化),那么修复它的唯一方法是在实现级别。
push-if
代码如下:
push-if
示例扩展:
(defmacro push-if (new-value predicate-fun list-place &environment env)
(multiple-value-bind (temp-syms val-forms
store-vars store-form access-form)
(get-setf-expansion list-place env)
(let ((old-val (gensym)))
(when (rest store-vars)
(error "PUSH-IF: cannot take ref of multiple-value place"))
`(multiple-value-bind (,@temp-syms) (values ,@val-forms)
(let ((,old-val ,access-form))
(when (funcall ,predicate-fun ,old-val)
(setf ,(first store-vars) (cons ,new-value ,old-val))
,store-form))))))
当地方是变量时,看起来很简单。我不打算解决一个小问题:表单> (macroexpand '(push-if new test place))
(LET* ((#:VALUES-12731 (MULTIPLE-VALUE-LIST (VALUES))))
(LET ((#:G12730 PLACE))
(WHEN (FUNCALL TEST #:G12730) (SETF #:NEW-12729 (CONS NEW #:G12730))
(SETQ PLACE #:NEW-12729)))) ;
,new
和test
每次只评估一次,但不按从左到右的顺序进行评估!
使用哈希表位置(CLISP)进行测试:
place
阿哈;现在生成了一些更有趣的代码,以避免两次评估> (macroexpand '(push-if new test (gethash a b)))
(LET*
((#:VALUES-12736 (MULTIPLE-VALUE-LIST (VALUES A B)))
(#:G12732 (POP #:VALUES-12736)) (#:G12733 (POP #:VALUES-12736)))
(LET ((#:G12735 (GETHASH #:G12732 #:G12733)))
(WHEN (FUNCALL TEST #:G12735) (SETF #:G12734 (CONS NEW #:G12735))
(SYSTEM::PUTHASH #:G12732 #:G12733 #:G12734)))) ;
和a
。 b
函数被调用一次,但其参数是gensym变量。旧值被捕获为gethash
。测试将应用于它,如果它通过,则存储变量#:G12735
将使用旧的列表值进行更新,并在其前面加上#:G12734
。然后,使用new
将该值放入哈希表中。
因此,在这个Lisp实现中,没有办法避免两个哈希表操作来执行更新:system::puthash
和gethash
。这是我们能做的最好的事情,并希望这两者作为优化配对。
答案 2 :(得分:0)
一些解决方法可能是:
如果常见模式是查找 - >找到它 - &gt; overwrite-it,然后你可以将值类型替换为包含值类型的列表。然后在找到键的值对象之后,只需破坏性地替换它的第一个元素,例如
(defun try-add (i)
(let ((old-i-list (gethash complex-list table nil)))
(if (may-add (first old-i-list))
(setf (first old-i-list) i) ; overwrite without searching again
(setf (gethash complex-list table) (list i))))) ; not there? too bad, we have to gethash again
或者,如果常见模式更像是查找 - >它不在那里 - &gt;添加它,你可能想要考虑自己散列键,然后让哈希表使用你的散列值作为键。这可能会更复杂,具体取决于这些复杂列表的深度和语义。在简单的情况下,您可能会使用散列函数(递归地)xor是其列表参数的元素的散列值。
EDITED:回答评论中的问题:我们的想法是,哈希表不是将哈希表映射到值的值,而是将键映射到单个元素列表,其中元素是值。然后,您可以更改这些列表的内容,而无需触及哈希表本身。以下是SBCL:
* (defparameter *my-hash* (make-hash-table))
*MY-HASH*
* (setf (gethash :my-key *my-hash*) (list "old-value"))
("old-value")
* (gethash :my-key *my-hash*)
("old-value")
T
* (defparameter old-value-container (gethash :my-key *my-hash*))
OLD-VALUE-CONTAINER
* (setf (first old-value-container) "new value")
"new value"
* (gethash :my-key *my-hash*)
("new value")
T
答案 3 :(得分:0)
您可以做的一件事是使用defstruct创建一个值,哈希表中的每个条目都指向该值。您的值列表(您在当前示例中所推荐的)可以存储在那里。结构创建可以在初始gethash调用中完成(作为默认值),也可以手动完成,如果你发现那里没有值。然后,对象可以按照您正在进行的方式进行副作用。
(这忽略了你是否真的想要将这些复杂的值用作哈希表键,或者是否有办法解决这个问题的问题。例如,你可能使用结构/ CLOS对象而不是复杂的列出作为你的密钥,然后你可以使用EQ哈希表。但这很大程度上取决于你正在做什么。)
答案 4 :(得分:0)
“分析表明,相同的测试需要很长时间。”
是的,但您是否确认#'EQUAL 哈希表查找也需要花费大量时间?
你有没有在SBCL之类的优化编译器上编译这个以获得速度并查看编译器注释?
解决了这两个问题后,您还可以为列表键的每个级别尝试嵌套哈希表。为任意嵌套的哈希表编写一个宏应该不难。
答案 5 :(得分:0)
也许我错过了一些明显的东西,但是:
(defun try-add (i)
(let ((old-i (gethash complex-list table)))
(when (may-add old-i)
(push i old-i))))
自:
编辑:oops,我是:我错过了old-i为零的情况。但如果这不是常见的情况,那么它仍然可能是一场胜利,因为在这种情况下你只需要进行查找:
(defun try-add (i)
(let ((old-i (gethash complex-list table)))
(when (may-add old-i)
(if old-i
(push i old-i)
(push i (gethash complex-list table))))))
嗯,那有用吗?