Es posible que en realidad esté accediendo a la tabla hash tres veces. ¿Por qué? Porque la macro push
se puede expandir en un código que hace un gethash
para obtener la lista, y luego alguna operación system::sethash
para almacenar el valor.
En este problema, está inspeccionando el valor de un lugar, que es una lista. Si esa lista satisface alguna prueba de predicado, entonces empuja algo hacia ese lugar.
Este problema puede ser atacado por la creación de operador de propósito especial que captura esta semántica:
(push-if <new-value> <predicate> <place>)
Por ejemplo:
(push-if i #'may-add (gethash complex-list table))
Este push-if
se define como un macro que utiliza la función de get-setf-expansion
en el argumento de forma <place>
para obtener las piezas necesarias para generar el código para acceder a ese lugar una sola vez.
El código generado evalúa una forma de carga para obtener el valor anterior del lugar, luego aplica la condición al valor anterior y, si tiene éxito, prepara el nuevo valor en la variable de almacenamiento temporal apropiada obtenida de get-setf-expansion
y evalúa el formulario de la tienda.
Esto es lo mejor que puede hacer en Lisp portátil, y es posible que todavía realice dos operaciones de hash, como se mencionó anteriormente. (En cuyo caso se espera hay una optimización de almacenamiento en caché decente en la propia tabla hash, pero al menos se ha reducido a dos operaciones..)
El enfoque será tan optimizado como el construido en el lugar mutando formas: incf
, push
, rotatef
, etc. Nuestra push-if
estará a la par de las incorporadas.
Si aún es asqueroso (realiza dos hash para actualizar un hash, sin optimización del almacenamiento en caché), la única forma de solucionarlo es en el nivel de implementación.
push-if
código sigue: expansión
(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))))))
muestra:
> (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)))) ;
Parece cuerdo para el caso simple cuando el lugar es una variable. Solo hay un pequeño problema que no voy a solucionar: los formularios new
, test
y place
se evalúan una sola vez, pero no de izquierda a derecha.
prueba con un lugar de la tabla de hash (CLISP):
> (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)))) ;
Aha; ahora hay un código algo más interesante que se genera para evitar evaluar a
y b
dos veces. La función gethash
se invoca una vez, pero sus argumentos son variables gensym. El valor anterior se captura como #:G12735
. Se aplica la prueba y, si se aprueba, la tienda variabel #:G12734
se actualiza con el valor anterior de la lista con new
en frente. Entonces, ese valor se pone en la tabla hash con system::puthash
.
Por lo tanto, en esta implementación de Lisp, no hay forma de evitar dos operaciones de la tabla hash para realizar una actualización: gethash
y system::puthash
. Esto es lo mejor que podemos hacer y esperamos que los dos funcionen como un par optimizado.
Genial - gracias :) –
Parece que podemos agradecer a Paul F. Dietz: http://git.boinkor.net/gitweb/sbcl.git/commitdiff/bc1783335d78be988465e4fc7cf9c5fdb88a3fa4 –