如何在Common Lisp中重用gethash查找?

时间:2009-07-08 17:37:42

标签: lisp common-lisp

我有一个哈希表,其中键是相当复杂的列表,带有符号和整数的子列表,并且应根据已存在的值修改值。该表是使用: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中完成。有什么想法吗?

6 个答案:

答案 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-expansionincfpush等。我们的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)))) ; newtest每次只评估一次,但不按从左到右的顺序进行评估!

使用哈希表位置(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)))) ; ab函数被调用一次,但其参数是gensym变量。旧值被捕获为gethash。测试将应用于它,如果它通过,则存储变量#:G12735将使用旧的列表值进行更新,并在其前面加上#:G12734。然后,使用new将该值放入哈希表中。

因此,在这个Lisp实现中,没有办法避免两个哈希表操作来执行更新:system::puthashgethash。这是我们能做的最好的事情,并希望这两者作为优化配对。

答案 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))))

自:

  • nil已经是GETHASH的默认值
  • GETHASH拉出整个对象,这样你就可以就地修改它而不是告诉PUSH如何再查找它
  • (样式点:当没有else子句时使用WHEN而不是IF)

编辑: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))))))
嗯,那有用吗?