Re: master: Process smashed weak hash-table cells more quickly

Stas Boukarev <[email protected]>
Newsgroups gmane.lisp.steel-bank.cvs,gmane.lisp.steel-bank.devel
Message-ID <CAF63=12nB3nzy24rXXKBBVyJS7CzGKpnWXEXdR+cMNcsALfK=Q@mail.gmail.com>
// Running ctor.impure.lisp in COMPILE evaluator mode
While evaluating the form starting at line 37, column 0
  of #P"/home/runner/work/sbcl/sbcl/tests/ctor.impure.lisp":
Unhandled UNDEFINED-FUNCTION: The function COMMON-LISP:NIL is undefined.
Backtrace for: #<SB-THREAD:THREAD tid=60184 "main thread" RUNNING {1200030003}>
0: ("undefined function")
1: ((LAMBDA NIL :IN "/home/runner/work/sbcl/sbcl/tests/ctor.impure.lisp"))
2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ASSERT (TYPEP (FUNCALL (GETHASH
(QUOTE #) SB-PCL::*ALL-CTORS*)) (QUOTE NO-SLOTS))) #<NULL-LEXENV>)

On Mon, Dec 8, 2025 at 2:52 AM snuglas via Sbcl-commits
<[email protected]> wrote:
>
> The branch "master" has been updated in SBCL:
>        via  4450de63ff2aa1d5664468ae401e65dd38c01f20 (commit)
>       from  58c43d7c837f44ad39a704dea19cff31138d4889 (commit)
>
> - Log -----------------------------------------------------------------
> commit 4450de63ff2aa1d5664468ae401e65dd38c01f20
> Author: Douglas Katzman <[email protected]>
> Date:   Sun Dec 7 18:51:05 2025 -0500
>
>     Process smashed weak hash-table cells more quickly
>
>     The first (SETF GETHASH) operation on a given table after a GC now deletes
>     the entire list that GC constructed to transfer the indices of culled cells
>     to Lisp. This is not great because the list is consed in alloc_generation
>     following any particular collect_generation. That is, we can't always
>     allocate to generation 0. Nor can the construction of such list be delayed
>     until just before returning from collect_garbage, because that would lose
>     information about what was clobbered. So at worst, we still cons the list
>     in a high generation only because we are forced to.
>
>     There are a couple of solutions but none readily at hand.
>     My sense is that we should change weak tables to using an open-addressing
>     strategy wherein it would be legal to stuff tombstones into k/v cells
>     during GC, and have probing figure out what happened. (Like maybe
>     rehashing the table if there are too many tombstones)
> ---
>  src/code/target-hash-table.lisp | 87 +++++++++++++++++++++++++----------------
>  src/runtime/gc-common.c         |  7 +++-
>  2 files changed, 60 insertions(+), 34 deletions(-)
>
> diff --git a/src/code/target-hash-table.lisp b/src/code/target-hash-table.lisp
> index 28b32351f..fbebdfe78 100644
> --- a/src/code/target-hash-table.lisp
> +++ b/src/code/target-hash-table.lisp
> @@ -13,6 +13,8 @@
>  (in-package "SB-IMPL")
>
>
> +(defvar *show-putweak* nil)
> +
>  (!begin-collecting-cold-init-forms)
>
>  (declaim (ftype (sfunction (hash-table t maybe-truncated-hash)
> @@ -2649,37 +2651,51 @@ nnnn 1_    any       linear scan (don't try to read when rehash already in progr
>    (declare (ignore default))
>    (%puthash key table new-value))
>
> -(defun hash-table-next-smashed-kv (hash-table)
> +(defun transfer-culled-cells (hash-table)
>    ;; Entries culled by GC are linked into a plain old list of cons cells,
>    ;; because we can atomically manipulate that. We can't atomically operate
>    ;; on the array-qua-list representation, both because we don't support
>    ;; numeric arrays in (CAS AREF) and lockfree deletion from interior nodes
>    ;; of singly-linked lists is tricky (a concurrent insert can get lost).
> -  (when (hash-table-smashed-cells hash-table)
> -    (binding* ((data (atomic-pop (hash-table-smashed-cells hash-table)))
> -               ((kv-index bucket) (etypecase data
> -                                   (fixnum (values (ldb (byte 14 14) data)
> -                                                   (ldb (byte 14 0) data)))
> -                                   (cons (values (car data) (cdr data)))))
> -               (index-vector (hash-table-index-vector hash-table))
> -               (next-vector (hash-table-next-vector hash-table))
> -               (this (aref index-vector bucket))
> -               (successor (aref next-vector this)))
> -      (if (= kv-index this)
> -          ;; This pair started a chain. Removing it is easy
> -          (setf (aref index-vector bucket) successor)
> -          ;; Else, find the kv-index in the chain and snap it out.
> -          (do ((predecessor this)
> -               (this successor))
> -              ((= this 0) (signal-corrupt-hash-table hash-table))
> -            (let ((successor (aref next-vector this)))
> -              (when (= kv-index this)
> -                (return (setf (aref next-vector predecessor) successor)))
> -              (setq predecessor this this successor))))
> -      ;; Set the 'next' at kv-index to the head of the ordinary freelist
> -      ;; so that when INSERT-AT pops the freelist, it stays correct.
> -      (setf (aref next-vector kv-index) (hash-table-next-free-kv hash-table))
> -      kv-index)))
> +  (unless (hash-table-smashed-cells hash-table)
> +    (return-from transfer-culled-cells))
> +  (let ((index-vector (hash-table-index-vector hash-table))
> +        (next-vector (hash-table-next-vector hash-table))
> +        (list (hash-table-smashed-cells hash-table)))
> +    (loop ; take ownership of the entire list in one CAS operation
> +     (let ((old (cas (hash-table-smashed-cells hash-table) list nil)))
> +       (if (eq old list) (return))
> +       (setq list old)))
> +    ;; TODO: special case if count = 0 then just reestablish the
> +    ;; initial conditions without processing one cell at a time.
> +    (loop
> +      (when (null list) (return))
> +      (binding* ((data (car list))
> +                 ((kv-index bucket) (etypecase data
> +                                      (fixnum (values (ldb (byte 14 14) data)
> +                                                      (ldb (byte 14 0) data)))
> +                                      (cons (values (car data) (cdr data)))))
> +                 (this (aref index-vector bucket))
> +                 (successor (aref next-vector this)))
> +        (let ((cdr (cdr list)))
> +          ;; Break up the the list in case GC sees an interior cell as an ambiguous root
> +          (rplacd list 0)
> +          (setq list cdr))
> +        (if (= kv-index this)
> +            ;; This pair started a chain. Removing it is easy
> +            (setf (aref index-vector bucket) successor)
> +            ;; Else, find the kv-index in the chain and snap it out.
> +            (do ((predecessor this)
> +                 (this successor))
> +                ((= this 0) (signal-corrupt-hash-table hash-table))
> +              (let ((successor (aref next-vector this)))
> +                (when (= kv-index this)
> +                  (return (setf (aref next-vector predecessor) successor)))
> +                (setq predecessor this this successor))))
> +        ;; Update the list headed by hash-table-next-free-kv and linked
> +        ;; through the 'next' vector.
> +        (setf (aref next-vector kv-index) (hash-table-next-free-kv hash-table)
> +              (hash-table-next-free-kv hash-table) kv-index)))))
>
>  ;;; We don't need the looping and checking for GC activiy in PUTHASH
>  ;;; because insertion can not co-occur with any other operation,
> @@ -2853,13 +2869,17 @@ nnnn 1_    any       linear scan (don't try to read when rehash already in progr
>
>    (defun puthash/weak (key hash-table value)
>      (declare (type hash-table hash-table) (optimize speed))
> +    ;; Delaying the transfer of culled cells could causee the linked list
> +    ;; denoting the cells to itself be promoted into a higher generation.
> +    ;; Ideally the list would be off-heap and not subject to GC. Attempting to
> +    ;; do that would engender two other problems: (1) knowing when to free the list
> +    ;; if the table becomes garbage and we had not yet processed - and thus freed -
> +    ;; the list. (2) GC can't call malloc()
> +    (transfer-culled-cells hash-table)
>      (with-weak-hash-table-entry
>        (declare (ignore predecessor))
>        (cond ((= physical-index 0)
> -             ;; There are two kinds of freelists. Prefer a smashed cell
> -             ;; so that we might shorten the chain it belonged to.
> -             (insert-at (or (hash-table-next-smashed-kv hash-table)
> -                            (hash-table-next-free-kv hash-table))
> +             (insert-at (hash-table-next-free-kv hash-table)
>                          hash-table key hash address-sensitive-p value))
>              ((or (empty-ht-slot-p (cas (weak-kvv-ref kv-vector (1+ physical-index))
>                                         probed-value value))
> @@ -3240,19 +3260,20 @@ table itself."
>  (defun hash-table-freelist (tbl)
>    (hash-table-chain tbl (hash-table-next-free-kv tbl)))
>
> -(defun show-chains (tbl &aux (nv (hash-table-next-vector tbl))
> +(defun show-chains (tbl &optional print &aux (nv (hash-table-next-vector tbl))
>                          (tot-len 0) (max-len 0) (n-chains 0))
> -  (flet ((show-chain (label next &aux (len 0))
> +  (flet ((show-chain (label next &optional print &aux (len 0))
>             (unless (eql next 0)
>               (write-string label)
>               (loop (format t " ~d" next)
> +                   (when print (format t "=~A" (aref (hash-table-pairs tbl) (ash next 1))))
>                     (incf len)
>                     (when (zerop (setq next (aref nv next))) (return)))
>               (terpri))
>             len))
>      (loop for x across (hash-table-index-vector tbl)
>            for i from 0
> -          do (let ((len (show-chain (format nil "Bucket ~d:" i) x)))
> +          do (let ((len (show-chain (format nil "Bucket ~d:" i) x print)))
>                 (when (plusp len)
>                   (incf tot-len len)
>                   (setf max-len (max len max-len))
> diff --git a/src/runtime/gc-common.c b/src/runtime/gc-common.c
> index 2a5f686fd..ace25d5ec 100644
> --- a/src/runtime/gc-common.c
> +++ b/src/runtime/gc-common.c
> @@ -1761,7 +1761,12 @@ scav_vector_t(lispobj *where, lispobj header)
>  }
>
>  /* Walk through the chain whose first element is *FIRST and remove
> - * dead weak entries.
> + * dead weak entries. Such entries are linked into a list which is distinct
> + * from the list of free entries linked through the table's "next" vector.
> + * Because GC can run in the middle of any hash-table modification -
> + * since we no longer use WITHOUT-GCING around every weak table operation -
> + * this has to avoid touching the table structure itself. But it's fairly easy
> + * to create an ordinary list which is amenable to SB-EXT:ATOMIC-POP.
>   * Return the new value for 'should rehash'.
>   *
>   * This operation might have to touch a hash-table that is currently
>
> -----------------------------------------------------------------------
>
>
> hooks/post-receive
> --
> SBCL
>
>
> _______________________________________________
> Sbcl-commits mailing list
> [email protected]
> https://lists.sourceforge.net/lists/listinfo/sbcl-commits


_______________________________________________
Sbcl-commits mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sbcl-commits
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.