mirror of
git://git.code.sf.net/p/sbcl/sbcl
synced 2026-09-10 07:26:40 -04:00
Add GC support for lockfree singly-linked lists
Based on paper by Tim Harris @ https://timharris.uk/papers/2001-disc.pdf. The algorithm depends on arbitrarily manipulation of 1 bit of a pointer which we can do by relaxing the tagging requirement for pointers from a node to its successor. For technical reasons, our representation differs from that in the reference paper - for us, the lowtag bits of a successor pointer are either all correct (INSTANCE_POINTER_LOWTAG), or all 0. The code has been lightly exercised on arm64, ppc, x86, and x86-64. I'm still deciding on the interface that we want to expose, and where such lists can be used internally.
This commit is contained in:
parent
ac1192682c
commit
75828dee01
|
|
@ -704,6 +704,7 @@
|
|||
;; It could be done in genesis, but not earlier,
|
||||
;; since the host has a package of that name.
|
||||
"src/code/defpackage"
|
||||
"src/code/target-lflist"
|
||||
"src/pcl/walk") ; needs DEFPACKAGE
|
||||
|
||||
#+sb-fasteval
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ case $1 in
|
|||
checkout="echo not syncing remote"
|
||||
;;
|
||||
*)
|
||||
echo "Usage error: cross-make.sh {sync|head|nosync} host port [env]"
|
||||
echo "Usage error: cross-make.sh {sync|head|nosync} host dir [env]"
|
||||
exit 1
|
||||
esac
|
||||
shift
|
||||
|
|
|
|||
|
|
@ -465,5 +465,7 @@ sb-c::
|
|||
((neq name new)
|
||||
(setf (%instance-ref debug-fun
|
||||
(get-dsd-index compiled-debug-fun name))
|
||||
new)))))))))))
|
||||
new)))))))
|
||||
(sb-lfl::linked-list
|
||||
(sb-lfl::finalize-deletion obj))))))
|
||||
:all))))
|
||||
|
|
|
|||
289
src/code/target-lflist.lisp
Normal file
289
src/code/target-lflist.lisp
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
;;;; Lockfree singly-linked lists
|
||||
;;;; using the algorithm of https://timharris.uk/papers/2001-disc.pdf.
|
||||
;;;; The algorithm as described requires being able to change the
|
||||
;;;; low-order bit of a pointer from 0 to 1 to mark pending deletions.
|
||||
;;;; Java implementations support this through a wrapper object
|
||||
;;;; known as AtomicMarkableReference.
|
||||
;;;; SBCL directly supports the mark bit by using the lowtag.
|
||||
|
||||
;;;; This software is part of the SBCL system. See the README file for
|
||||
;;;; more information.
|
||||
;;;;
|
||||
;;;; This software is derived from the CMU CL system, which was
|
||||
;;;; written at Carnegie Mellon University and released into the
|
||||
;;;; public domain. The software is in the public domain and is
|
||||
;;;; provided with absolutely no warranty. See the COPYING and CREDITS
|
||||
;;;; files for more information.
|
||||
|
||||
(defpackage "SB-LFL"
|
||||
(:use "CL" "SB-EXT" "SB-INT" "SB-SYS" "SB-KERNEL"))
|
||||
|
||||
(in-package "SB-LFL")
|
||||
|
||||
;;; The changes to GC to support this code were as follows:
|
||||
;;; * One bit of the payload length in an instance header is reserved to signify
|
||||
;;; that the instance has a special GC scavenge method. This avoids indirecting
|
||||
;;; to the layout to see whether all instances of a type have a special method.
|
||||
;;;
|
||||
;;; * If an instance has a header bit so indicating, then the first data slot
|
||||
;;; is treated as an instance pointer even if it missing its tag bits.
|
||||
;;;
|
||||
;;; * Since you can't pin an object that you don't a-priori have a tagged pointer
|
||||
;;; to, pinning a lockfree list node may implicitly pin not only that node but
|
||||
;;; also the successor node, since there would otherwise be no way to reconstruct
|
||||
;;; (in Lisp) a tagged pointer to the successor of a node pending deletion.
|
||||
;;;
|
||||
;;; * Copying a lockfree list node tries to copy the successor nodes into adjacent
|
||||
;;; memory just like copying a chain of cons cells. This is inessential but nice.
|
||||
;;;
|
||||
;;; * verify_range() knows how to verify the 'next' pointer even when it looks like
|
||||
;;; a fixnum. Without this it would have been more difficult to test the above.
|
||||
;;;
|
||||
;;; The remaining issue is relatively unimportant: neither traceroot nor
|
||||
;;; DO-REFERENCED-OBJECT can follow untagged pointers.
|
||||
;;; This is potentially more of an annoyance than it is a bug.
|
||||
|
||||
(defstruct (node (:conc-name nil)
|
||||
(:constructor %make-node (node-key node-data)))
|
||||
;; Using either 0 or NIL as the 'next' would make sense for the final cell.
|
||||
;; 0 makes things easier for C, but NIL makes things easier for Lisp.
|
||||
;; Using NIL simplifies the test in MARKEDP+NEXT so that the condition
|
||||
;; for ORing in tag bits is simply whether 'next' is a fixnum.
|
||||
;; Using 0 would require checking for fixnum and non-zero.
|
||||
(%node-next nil)
|
||||
(node-data)
|
||||
(node-key 0 :read-only t))
|
||||
;;; Change the layout bitmap from -1 to a bitmap with 1s for each tagged slot.
|
||||
;;; These are essentially equivalent, however -1 indicates that all slots are
|
||||
;;; tagged *and* that there is no special scavenge method.
|
||||
;;; A positive number _may_ indicate that all slots are tagged, but also
|
||||
;;; informs the scavenge that there may be a custom action as well.
|
||||
(let ((layout (sb-kernel:find-layout 'node)))
|
||||
(setf (layout-bitmap layout)
|
||||
;; Round to odd, make any padding slot tagged.
|
||||
;; If we allow subtypes of NODE, then the bitmap will have to
|
||||
;; be fixed up as well.
|
||||
(1- (ash 1 (logior (layout-length layout) 1)))))
|
||||
|
||||
(defconstant special-gc-strategy-flag #x800000)
|
||||
(declaim (ftype (sfunction (t t) node) make-node))
|
||||
(defun make-node (key data)
|
||||
(declare (inline %make-node))
|
||||
(let ((n (%make-node key data)))
|
||||
;; FIXME: this bit needs to be frobbed with a vop, or better yet,
|
||||
;; just set in the allocator; and should (ideally) be made atomic.
|
||||
;; Note that SET-HEADER-DATA only works on OTHER-POINTER-LOWTAG.
|
||||
(with-pinned-objects (n)
|
||||
(let ((sap (int-sap (get-lisp-obj-address n))))
|
||||
(setf (sap-ref-word sap (- sb-vm:instance-pointer-lowtag))
|
||||
(logior special-gc-strategy-flag
|
||||
(sap-ref-word sap (- sb-vm:instance-pointer-lowtag))))))
|
||||
n))
|
||||
|
||||
(define-load-time-global *tail-atom* (make-node nil :tail))
|
||||
|
||||
;;; Specialized list variants will be created for
|
||||
;;; fixnum, integer, real, string, generic "comparable"
|
||||
;;; but the node type and list type is the same regardless of key type.
|
||||
(defstruct (linked-list (:constructor %make-lfl)
|
||||
(:conc-name list-))
|
||||
(head nil :type node)
|
||||
(tail nil :type node))
|
||||
|
||||
(defun new-lockfree-list ()
|
||||
(let ((head (make-node nil :head))
|
||||
(tail *tail-atom*))
|
||||
(setf (%node-next head) tail)
|
||||
(%make-lfl :head head :tail tail)))
|
||||
|
||||
;;; "Marked" in the reference algorithm means ORing in a 1 to the low bit.
|
||||
;;; For us it means *removal* of tag bits.
|
||||
;;; MAKE-MARKED-REF can only be called in the scope of WITH-PINNED-OBJECTS.
|
||||
;;; The critical invariant is that once a 'next' pointer has been turned into
|
||||
;;; a fixnum, it CAN NOT change. Therefore, the object that GC implicitly pins
|
||||
;;; - along with the explicit pin of NODE within MARKED+NEXT - is definitely the
|
||||
;;; object whose tagged pointer is reconstructed. This is exactly why we choose
|
||||
;;; the tagged state as the normal state and the untagged state as deleted.
|
||||
;;; If that were reversed (so tag bits = deleted, no tag bits = normal) to be like
|
||||
;;; the reference algorithm, wherein "marked" = "deleted", object pinning
|
||||
;;; could fail. A competing thread could CAS the untagged bits, invoke GC, while we
|
||||
;;; try to reconstructed a different object from bits that were read prior to the CAS
|
||||
;;; and prior to the GC, during which time the object to reconstruct moved.
|
||||
(declaim (inline make-marked-ref))
|
||||
(defun make-marked-ref (x)
|
||||
(%make-lisp-obj (logandc2 (get-lisp-obj-address x) sb-vm:lowtag-mask)))
|
||||
|
||||
(declaim (inline markedp markedp+next))
|
||||
(defun markedp (node) (fixnump (%node-next node)))
|
||||
(defun markedp+next (node)
|
||||
(with-pinned-objects (node) ; pinning a node also pins its 'next'
|
||||
(let ((next (%node-next node)))
|
||||
(if (fixnump next)
|
||||
(values t (truly-the node
|
||||
(%make-lisp-obj (logior (get-lisp-obj-address next)
|
||||
sb-vm:instance-pointer-lowtag))))
|
||||
(values nil (truly-the node next))))))
|
||||
|
||||
(defun node-next (node)
|
||||
(nth-value 1 (markedp+next node)))
|
||||
|
||||
(defmethod print-object ((list linked-list) stream)
|
||||
(print-unreadable-object (list stream :type t)
|
||||
(write-char #\{ stream)
|
||||
(let ((node (%node-next (list-head list))))
|
||||
(unless (eq node (list-tail list))
|
||||
(loop (multiple-value-bind (deleted next) (markedp+next node)
|
||||
(when deleted
|
||||
(write-char #\* stream))
|
||||
(write (node-key node) :stream stream)
|
||||
(setq node next)
|
||||
(when (eq node (list-tail list)) (return))
|
||||
(write-char #\space stream)))))
|
||||
(write-char #\} stream)))
|
||||
|
||||
(defmethod print-object ((node node) stream)
|
||||
(print-unreadable-object (node stream :type t)
|
||||
(format stream "(~:[~;*~]~D ~S)"
|
||||
(markedp node)
|
||||
(node-key node)
|
||||
(node-data node))))
|
||||
|
||||
(defmacro do-lockfree-list ((var list &optional result) &body body)
|
||||
`(let* ((.list. ,list)
|
||||
(.end. (list-tail .list.))
|
||||
(,var (%node-next (list-head .list.))))
|
||||
(loop
|
||||
(when (eq ,var .end.) (return ,result))
|
||||
(multiple-value-bind (.mark. .next.) (markedp+next (truly-the node ,var))
|
||||
(unless .mark. (let ((,var ,var)) (declare (ignorable ,var)) ,@body))
|
||||
(setq ,var .next.)))))
|
||||
|
||||
(defun lfl-length (list) ; a snapshot at a point in time
|
||||
(let ((n 0))
|
||||
(do-lockfree-list (x list) (incf n))
|
||||
n))
|
||||
|
||||
;;; SEARCH returns a pair of nodes satisfying the following constraints:
|
||||
;;; - key(left) < search-key and key(right) >= search-key
|
||||
;;; - neither left nor right is marked for deletion
|
||||
;;; - right is the immediate successor of left
|
||||
;;; Any logically deleted nodes in between left and right will be removed.
|
||||
(defmacro lfl-search-macro (compare< type)
|
||||
`(block search
|
||||
(let (left left-node-next right (tail (list-tail list)))
|
||||
(tagbody
|
||||
again
|
||||
;; 1. Find left and right nodes
|
||||
(binding* ((this (list-head list))
|
||||
((markedp next) (markedp+next this)))
|
||||
(loop (unless markedp
|
||||
(setq left this left-node-next next))
|
||||
(when (eq (setq this next) tail)
|
||||
(return))
|
||||
(multiple-value-setq (markedp next) (markedp+next this))
|
||||
(unless (or markedp (,compare< (truly-the ,type (node-key this))
|
||||
key))
|
||||
(return)))
|
||||
(setq right this))
|
||||
;; 2. Check adjacency
|
||||
(when (eq left-node-next right)
|
||||
(if (and (neq right tail) (markedp right))
|
||||
(go again)
|
||||
(return-from search (values right left))))
|
||||
;; 3. Remove intervening marked nodes
|
||||
(when (eq (cas (%node-next (truly-the node left)) left-node-next right)
|
||||
left-node-next)
|
||||
(unless (and (neq right tail) (markedp right))
|
||||
(return-from search (values right left))))
|
||||
(go again)))))
|
||||
|
||||
;;; This is pretty much the standard CAS-based atomic list insert algorithm.
|
||||
(defmacro lfl-insert-macro (search compare= type)
|
||||
`(let ((new (make-node key data)))
|
||||
(loop
|
||||
;; LEFT and RIGHT are the nodes bracketing the insertion point.
|
||||
(multiple-value-bind (right left) (,search list key)
|
||||
(when (and (neq right (list-tail list))
|
||||
(,compare= key (truly-the ,type (node-key right))))
|
||||
(return nil))
|
||||
(setf (%node-next new) right)
|
||||
(when (eq (cas (%node-next left) right new) right)
|
||||
(return new))))))
|
||||
|
||||
;;; Deletion
|
||||
;;; Step 1: find the node to be deleted
|
||||
;;; Step 2: mark it as pending deletion in the 'next' slot
|
||||
;;; Step 3: swing the predecessor's next to the successor of deleted node.
|
||||
;;;
|
||||
;;; Example: After step 2 of deleting node C we have:
|
||||
;;; A --> B --> C --> D
|
||||
;;; ^ (mark)
|
||||
;;; If swapping node B's 'next' fails, then some operation occurred to the left.
|
||||
;;; Due to deletion the predecessor of C might become node A:
|
||||
;;; A --> C --> D
|
||||
;;; Due to insertion the predecessor of C might become node X:
|
||||
;;; A --> B --> X --> C --> D
|
||||
(defmacro lfl-delete-macro (search compare= type)
|
||||
`(loop
|
||||
;; Step 1: find
|
||||
(multiple-value-bind (this predecessor) (,search list key)
|
||||
(when (or (eql this (list-tail list))
|
||||
(not (,compare= key (truly-the ,type (node-key this)))))
|
||||
(return nil))
|
||||
(let ((succ (%node-next this)))
|
||||
(unless (fixnump succ)
|
||||
;; Pin here because we're taking the address of the successor object.
|
||||
;; Instead we could use bit-test-and-set on the x86 architecture.
|
||||
(with-pinned-objects (succ)
|
||||
;; Step 2: logically delete 'this'
|
||||
(when (eq (cas (%node-next this) succ (make-marked-ref succ)) succ)
|
||||
;; Step 3: physically delete by swinging the predecessor's successor
|
||||
(unless (eq succ (cas (%node-next predecessor) this succ))
|
||||
;; Call SEARCH again which will perform physical deletion.
|
||||
(,search list key))
|
||||
(return t))))))))
|
||||
|
||||
(defmacro define-variation (type compare< compare=)
|
||||
(let ((search (symbolicate "LFL-SEARCH/" type)))
|
||||
`(progn
|
||||
(declaim (ftype (sfunction (linked-list ,type) (values node node))
|
||||
,search))
|
||||
(defun ,search (list key) (lfl-search-macro ,compare< ,type))
|
||||
|
||||
(defun ,(symbolicate "LFL-INSERT/"type) (list key data)
|
||||
(declare (linked-list list) (,type key))
|
||||
(lfl-insert-macro ,search ,compare= ,type))
|
||||
|
||||
(defun ,(symbolicate "LFL-DELETE/"type) (list key)
|
||||
(declare (linked-list list) (,type key))
|
||||
(lfl-delete-macro ,search ,compare= ,type))
|
||||
|
||||
(defun ,(symbolicate "LFL-FIND/"type) (list key)
|
||||
(declare (linked-list list) (,type key))
|
||||
(let ((node (,search list key)))
|
||||
(when (and (neq node (list-tail list))
|
||||
(,compare= key (truly-the ,type (node-key node))))
|
||||
node))))))
|
||||
|
||||
(define-variation real < =) ; uses general case of math functions
|
||||
;; TODO: implement an INTEGER< assembly routine perhaps?
|
||||
(define-variation integer < =) ; comparator= reduces to INTEGER-EQL
|
||||
(define-variation fixnum < =)
|
||||
(define-variation string string< string=)
|
||||
|
||||
;;; SAVE-LISP-AND-DIE must unlink logically deleted nodes, because coreparse
|
||||
;;; would not understand how to followed untagged pointers in the event that
|
||||
;;; heap relocation had to occur on restart. Of course the only way to see
|
||||
;;; a logically deleted node here is if a deleting thread died a horrible
|
||||
;;; sudden death.
|
||||
;;; Each list will be processed exactly once.
|
||||
(defun finalize-deletion (list)
|
||||
(let* ((pred (list-head list))
|
||||
(node (node-next pred)))
|
||||
(loop
|
||||
(when (eq node (list-tail list))
|
||||
(return))
|
||||
(multiple-value-bind (markedp next) (markedp+next node)
|
||||
(if markedp
|
||||
(setf node next (%node-next pred) node)
|
||||
(setf pred node node next))))))
|
||||
|
|
@ -449,7 +449,8 @@
|
|||
(:result-types positive-fixnum)
|
||||
(:generator 4
|
||||
(loadw temp struct 0 instance-pointer-lowtag)
|
||||
(inst lsr res temp n-widetag-bits)))
|
||||
(inst ubfm res temp n-widetag-bits
|
||||
(+ -1 (integer-length short-header-max-words) n-widetag-bits))))
|
||||
|
||||
(define-full-reffer instance-index-ref * instance-slots-offset
|
||||
instance-pointer-lowtag (descriptor-reg any-reg) * %instance-ref)
|
||||
|
|
|
|||
|
|
@ -472,7 +472,9 @@
|
|||
(:result-types positive-fixnum)
|
||||
(:generator 4
|
||||
(loadw temp struct 0 instance-pointer-lowtag)
|
||||
(inst srwi res temp n-widetag-bits)))
|
||||
;; shift right 8 and mask 15 low bits =
|
||||
;; rotate left 24, take bit indices 17 through 31.
|
||||
(inst rlwinm res temp (- 32 n-widetag-bits) 17 31)))
|
||||
|
||||
(define-vop (instance-index-ref word-index-ref)
|
||||
(:policy :fast-safe)
|
||||
|
|
|
|||
|
|
@ -646,6 +646,7 @@
|
|||
(:result-types positive-fixnum)
|
||||
(:generator 4
|
||||
(inst movzx '(:word :dword) res (ea (1+ (- instance-pointer-lowtag)) struct))
|
||||
(inst and :dword res short-header-max-words) ; clear special GC bit
|
||||
(inst shl :dword res n-fixnum-tag-bits)))
|
||||
|
||||
#!+compact-instance-header
|
||||
|
|
|
|||
|
|
@ -458,7 +458,8 @@
|
|||
(:result-types positive-fixnum)
|
||||
(:generator 4
|
||||
(loadw res struct 0 instance-pointer-lowtag)
|
||||
(inst shr res n-widetag-bits)))
|
||||
(inst shr res n-widetag-bits)
|
||||
(inst and res short-header-max-words))) ; clear special GC bit
|
||||
|
||||
(define-full-reffer instance-index-ref *
|
||||
instance-slots-offset instance-pointer-lowtag
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
#include "genesis/gc-tables.h"
|
||||
#include "genesis/closure.h"
|
||||
#include "genesis/cons.h"
|
||||
#include "genesis/instance.h"
|
||||
#include "genesis/vector.h"
|
||||
#include "genesis/layout.h"
|
||||
#include "genesis/hash-table.h"
|
||||
|
|
@ -269,7 +270,16 @@ static void trace_object(lispobj* where)
|
|||
// mixed boxed/unboxed objects
|
||||
bitmap = ((struct layout*)native_pointer(layout))->bitmap;
|
||||
// If no raw slots, just scan without use of the bitmap.
|
||||
// A bitmap of -1 implies that not only are all slots tagged,
|
||||
// there is no special GC method for any slot.
|
||||
if (bitmap == make_fixnum(-1)) break;
|
||||
// Otherwise, the first slot might merit special treatment.
|
||||
if (*where & CUSTOM_GC_SCAVENGE_FLAG) {
|
||||
struct instance* node = (struct instance*)where;
|
||||
lispobj next = node->slots[INSTANCE_DATA_START];
|
||||
if (fixnump(next) && next) // ignore initially 0 heap words
|
||||
__mark_obj(next|INSTANCE_POINTER_LOWTAG);
|
||||
}
|
||||
for(i=1; i<scan_to; ++i)
|
||||
if (layout_bitmap_logbitp(i-1, bitmap) && is_lisp_pointer(where[i]))
|
||||
__mark_obj(where[i]);
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@ os_vm_size_t bytes_consed_between_gcs = 12*1024*1024;
|
|||
|
||||
/* Medium-sized payload count is expressed in 15 bits. Objects in this category
|
||||
* may reside in immobile space: CLOSURE, INSTANCE, FUNCALLABLE-INSTANCE.
|
||||
* The single data bit is used as a closure's NAMED flag.
|
||||
* The single data bit is used as a closure's NAMED flag,
|
||||
* or an instance's "special GC strategy" flag.
|
||||
*
|
||||
* Header: gen# | data | size | tag
|
||||
* ----- ----- ------- ------
|
||||
|
|
@ -476,19 +477,36 @@ trans_fun_header(lispobj object)
|
|||
* instances
|
||||
*/
|
||||
|
||||
static inline lispobj copy_instance(lispobj object)
|
||||
{
|
||||
// Object is an un-forwarded object in from_space
|
||||
lispobj header = *(lispobj*)(object - INSTANCE_POINTER_LOWTAG);
|
||||
lispobj copy = copy_object(object, 1 + (instance_length(header)|1));
|
||||
set_forwarding_pointer(native_pointer(object), copy);
|
||||
return copy;
|
||||
}
|
||||
|
||||
static sword_t
|
||||
scav_instance_pointer(lispobj *where, lispobj object)
|
||||
{
|
||||
gc_dcheck(instancep(object));
|
||||
lispobj header = *(lispobj*)(object - INSTANCE_POINTER_LOWTAG);
|
||||
/* Object is a pointer into from space - not a FP. */
|
||||
lispobj copy = copy_object(object, 1 + (instance_length(header)|1));
|
||||
|
||||
gc_dcheck(copy != object);
|
||||
|
||||
set_forwarding_pointer(native_pointer(object), copy);
|
||||
lispobj copy = copy_instance(object);
|
||||
*where = copy;
|
||||
|
||||
struct instance* node = (struct instance*)(copy - INSTANCE_POINTER_LOWTAG);
|
||||
// Copy chain of lockfree list nodes to consecutive memory addresses,
|
||||
// just like trans_list does. A logically deleted node will break the chain,
|
||||
// as its 'next' will not satisfy instancep(), but that's ok.
|
||||
if (node->header & CUSTOM_GC_SCAVENGE_FLAG) {
|
||||
while (instancep(object = node->slots[INSTANCE_DATA_START]) // node.next
|
||||
&& from_space_p(object)
|
||||
&& !forwarding_pointer_p(native_pointer(object))) {
|
||||
copy = copy_instance(object);
|
||||
node->slots[INSTANCE_DATA_START] = copy;
|
||||
node = (struct instance*)(copy - INSTANCE_POINTER_LOWTAG);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
|
@ -691,9 +709,28 @@ scav_instance(lispobj *where, lispobj header)
|
|||
lbitmap = ((struct layout*)layout)->bitmap;
|
||||
}
|
||||
sword_t nslots = instance_length(header) | 1;
|
||||
if (lbitmap == make_fixnum(-1))
|
||||
if (lbitmap == make_fixnum(-1)) {
|
||||
scavenge(where+1, nslots);
|
||||
else if (!fixnump(lbitmap)) {
|
||||
return 1 + nslots;
|
||||
}
|
||||
// Specially scavenge the 'next' slot of a lockfree list node. If the node is
|
||||
// pending deletion, 'next' will satisfy fixnump() but is in fact a pointer.
|
||||
// GC doesn't care too much about the deletion algorithm, but does have to
|
||||
// ensure liveness of the pointee, which may move unless pinned.
|
||||
// One could imagine that the strategy is further determind by layout->_flags.
|
||||
// A single bit suffices for now, but more generality is certainly possible.
|
||||
if (header & CUSTOM_GC_SCAVENGE_FLAG) {
|
||||
struct instance* node = (struct instance*)where;
|
||||
lispobj next = node->slots[INSTANCE_DATA_START];
|
||||
if (fixnump(next) && next) { // ignore initially 0 heap words
|
||||
lispobj descriptor = next | INSTANCE_POINTER_LOWTAG;
|
||||
scav1(&descriptor, descriptor);
|
||||
// Fix the pointer but of course leave it in mid-deletion (untagged) state.
|
||||
if (descriptor != (next | INSTANCE_POINTER_LOWTAG))
|
||||
node->slots[INSTANCE_DATA_START] = descriptor & ~LOWTAG_MASK;
|
||||
}
|
||||
}
|
||||
if (!fixnump(lbitmap)) {
|
||||
/* It is conceivable that 'lbitmap' points to from_space, AND that it
|
||||
* is stored in one of the slots of the instance about to be scanned.
|
||||
* If so, then forwarding it will deposit new bits into its first
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@
|
|||
// Even on cheneygc we need this flag, but it's actually just ignored.
|
||||
#define ALLOC_QUICK 1
|
||||
|
||||
#define CUSTOM_GC_SCAVENGE_FLAG 0x800000
|
||||
|
||||
#ifdef LISP_FEATURE_GENCGC
|
||||
#include "gencgc-alloc-region.h"
|
||||
void *
|
||||
|
|
|
|||
|
|
@ -1879,6 +1879,23 @@ pin_object(lispobj object)
|
|||
})
|
||||
}
|
||||
}
|
||||
if (lowtag_of(object) == INSTANCE_POINTER_LOWTAG
|
||||
&& (*(lispobj*)(object - INSTANCE_POINTER_LOWTAG)
|
||||
& CUSTOM_GC_SCAVENGE_FLAG)) {
|
||||
struct instance* instance = (struct instance*)(object - INSTANCE_POINTER_LOWTAG);
|
||||
// When pinning a logically deleted lockfree list node, always pin the
|
||||
// successor too, since the Lisp code will reconstruct the next node's tagged
|
||||
// pointer from the native pointer. Since we're still in the object pinning phase
|
||||
// of GC, layouts can't have been forwarded yet. In fact we don't use bits
|
||||
// from the layout, but it's worth noting, in case we needed to.
|
||||
// Note also that this 'pin' does not need to happen for mark-only GC.
|
||||
// The pin is from an address perspective, not a liveness perspective,
|
||||
// because the instance scavenger would correctly trace this reference.
|
||||
lispobj next = instance->slots[INSTANCE_DATA_START];
|
||||
// Be sure to ignore 0 words.
|
||||
if (fixnump(next) && next && from_space_p(next | INSTANCE_POINTER_LOWTAG))
|
||||
pin_object(next | INSTANCE_POINTER_LOWTAG);
|
||||
}
|
||||
}
|
||||
#else
|
||||
# define scavenge_pinned_ranges()
|
||||
|
|
@ -2697,6 +2714,16 @@ verify_range(lispobj *where, sword_t nwords, struct verify_state *state)
|
|||
lispobj bitmap = layout->bitmap;
|
||||
gc_assert(fixnump(bitmap)
|
||||
|| widetag_of(native_pointer(bitmap))==BIGNUM_WIDETAG);
|
||||
if (*where & CUSTOM_GC_SCAVENGE_FLAG) {
|
||||
struct instance* node = (struct instance*)where;
|
||||
lispobj next = node->slots[INSTANCE_DATA_START];
|
||||
if (fixnump(next) && next) {
|
||||
state->vaddr = &node->slots[INSTANCE_DATA_START];
|
||||
next |= INSTANCE_POINTER_LOWTAG;
|
||||
verify_range(&next, 1, state);
|
||||
state->vaddr = 0;
|
||||
}
|
||||
}
|
||||
instance_scan((void (*)(lispobj*, sword_t, uword_t))verify_range,
|
||||
where+1, nslots, bitmap, (uintptr_t)state);
|
||||
count = 1 + nslots;
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ static int find_ref(lispobj* source, lispobj target)
|
|||
bitmap = layout ? LAYOUT(layout)->bitmap : make_fixnum(-1);
|
||||
for(i=1; i<scan_limit; ++i)
|
||||
if (layout_bitmap_logbitp(i-1, bitmap)) check_ptr(i, source[i]);
|
||||
// FIXME: check CUSTOM_GC_SCAVENGE in the header
|
||||
return -1;
|
||||
#if FUN_SELF_FIXNUM_TAGGED
|
||||
case CLOSURE_WIDETAG:
|
||||
|
|
@ -718,6 +719,7 @@ static uword_t build_refs(lispobj* where, lispobj* end,
|
|||
check_ptr(layout);
|
||||
// Partially initialized instance can't have nonzero words yet
|
||||
bitmap = layout ? LAYOUT(layout)->bitmap : make_fixnum(-1);
|
||||
// FIXME: check CUSTOM_GC_SCAVENGE in the header
|
||||
// If no raw slots, just scan without use of the bitmap.
|
||||
if (bitmap == make_fixnum(-1)) break;
|
||||
for(i=1; i<scan_limit; ++i)
|
||||
|
|
|
|||
157
tests/lockfree-list.impure.lisp
Normal file
157
tests/lockfree-list.impure.lisp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
(in-package "SB-LFL")
|
||||
|
||||
#-(and sb-thread (or arm64 ppc x86 x86-64)) (sb-ext:exit :code 104)
|
||||
|
||||
;;;; These functions are for examining GC behavior.
|
||||
|
||||
(defun lfl-nth (n list)
|
||||
(let ((node (%node-next (list-head list))))
|
||||
(dotimes (i n node)
|
||||
(setq node (node-next node)))))
|
||||
|
||||
;;; For testing (especially the garbage collector), perform the first CAS
|
||||
;;; operation but not the second CAS of the deletion algorithm.
|
||||
(defun logical-delete (n list)
|
||||
(let* ((node (lfl-nth n list))
|
||||
(succ (%node-next node)))
|
||||
(unless (fixnump succ)
|
||||
(with-pinned-objects (succ)
|
||||
(cas (%node-next node) succ (make-marked-ref succ)))))
|
||||
list)
|
||||
|
||||
(defvar *lfl*)
|
||||
(defvar *l*)
|
||||
(flet ((show (node step when)
|
||||
(format t "~a: " when)
|
||||
(loop (format t "~x" (get-lisp-obj-address node))
|
||||
(unless (setq node (funcall step node)) (return))
|
||||
(format t " - "))
|
||||
(terpri)))
|
||||
(defun makelist (n)
|
||||
(setq *l* nil)
|
||||
(dotimes (i n) (push (cons i (format nil "~r" i)) *l*))
|
||||
(show *l* #'cdr "init")
|
||||
(setq *l* (nreverse *l*))
|
||||
(show *l* #'cdr "rev ")
|
||||
(gc)
|
||||
(show *l* #'cdr "GCed"))
|
||||
(defun makelflist (n &optional show)
|
||||
(setq *lfl* (new-lockfree-list))
|
||||
(dotimes (i n) (lfl-insert/fixnum *lfl* (* i n) (format nil "~r" i)))
|
||||
(when show (show (list-head *lfl*) #'node-next "init"))
|
||||
(gc)
|
||||
(when show (show (list-head *lfl*) #'node-next "GCed"))
|
||||
(logical-delete 1 *lfl*)
|
||||
(logical-delete 4 *lfl*)
|
||||
(gc :gen 2)
|
||||
(when show (show (list-head *lfl*) #'node-next "del "))))
|
||||
|
||||
(test-util:with-test (:name :lockfree-list-gc-correctness)
|
||||
;; Enable heap validity tester
|
||||
(setf (sb-alien:extern-alien "verify_gens" char) 0)
|
||||
;; Create a small list and perform logical deletion of 2 nodes
|
||||
(makelflist 10)
|
||||
(gc)) ; Verify no post-gc crash
|
||||
|
||||
(test-util:with-test (:name :lockfree-list-finalize-deletion)
|
||||
;; Check that save-lisp-and-die can remove deleted nodes
|
||||
(let ((list (new-lockfree-list)))
|
||||
(lfl-insert/fixnum list 4 "four")
|
||||
(lfl-insert/fixnum list 5 "five")
|
||||
(logical-delete 0 list)
|
||||
(logical-delete 1 list)
|
||||
(finalize-deletion list)
|
||||
(let* ((node (list-head list))
|
||||
(next (node-next node)))
|
||||
(assert (eq next (list-tail list))))))
|
||||
|
||||
;;; These functions are for comparing the running time of a lock-based
|
||||
;;; implementation versus lockfree.
|
||||
|
||||
(defun new-synchronized-list ()
|
||||
(list (sb-thread:make-mutex)))
|
||||
(defun list-search (list key)
|
||||
(let* (left
|
||||
(this list)
|
||||
(next (cdr this)))
|
||||
(loop (setq left this this next)
|
||||
(when (null this)
|
||||
(return))
|
||||
(setq next (cdr this))
|
||||
(unless (< (truly-the fixnum (caar this)) key)
|
||||
(return)))
|
||||
(values this left)))
|
||||
(defun locked-insert (list key value)
|
||||
(sb-thread:with-mutex ((car list))
|
||||
(multiple-value-bind (successor predecessor) (list-search list key)
|
||||
(let ((new (cons (cons key value) successor)))
|
||||
(rplacd predecessor new)))
|
||||
list))
|
||||
(defun locked-delete (list key)
|
||||
(sb-thread:with-mutex ((car list))
|
||||
(multiple-value-bind (this predecessor) (list-search list key)
|
||||
(when (and this (= (caar this) key))
|
||||
(rplacd predecessor (cdr this)))))
|
||||
list)
|
||||
|
||||
;;;
|
||||
|
||||
(defglobal *worklist* nil)
|
||||
(defmacro smoketest-macro (constructor inserter deleter)
|
||||
`(let ((list ,constructor)
|
||||
(threads))
|
||||
(assert (<= n-threads 50))
|
||||
(let ((max (* n-threads n-items)))
|
||||
(dotimes (i n-threads)
|
||||
(push (sb-thread:make-thread
|
||||
(lambda (my-items &aux (ct 0))
|
||||
(loop
|
||||
(let ((val (atomic-pop *worklist*)))
|
||||
(unless val (return))
|
||||
(setf (aref my-items ct) val)
|
||||
(,inserter list val (- val))
|
||||
(incf ct)
|
||||
(when (oddp ct)
|
||||
(let ((item-to-delete
|
||||
(aref my-items (floor ct 2))))
|
||||
(,deleter list item-to-delete))))))
|
||||
:arguments (make-array max))
|
||||
threads)))
|
||||
(dolist (thr threads) (sb-thread:join-thread thr))
|
||||
list))
|
||||
|
||||
(defun smoketest-lockfree (n-threads n-items)
|
||||
(smoketest-macro (new-lockfree-list) lfl-insert/fixnum lfl-delete/fixnum))
|
||||
(defun smoketest-locked (n-threads n-items)
|
||||
(smoketest-macro (new-synchronized-list)
|
||||
locked-insert locked-delete))
|
||||
|
||||
(defun primitive-benchmark (&key (n-trials 3) (n-threads 10) (n-items 500) print)
|
||||
(let ((best 0))
|
||||
(dotimes (trial n-trials best)
|
||||
(let* ((max (* n-threads n-items))
|
||||
(worklist (test-util:shuffle (test-util:integer-sequence max)))
|
||||
(random-state (make-random-state)))
|
||||
(let ((rt0 (get-internal-real-time))
|
||||
(rt1)
|
||||
(rt2))
|
||||
(setq *worklist* (copy-list worklist)
|
||||
*random-state* (make-random-state random-state))
|
||||
(smoketest-locked n-threads n-items)
|
||||
(setq rt1 (get-internal-real-time))
|
||||
(setq *worklist* (copy-list worklist)
|
||||
*random-state* (make-random-state random-state))
|
||||
(smoketest-lockfree n-threads n-items)
|
||||
(setq rt2 (get-internal-real-time))
|
||||
(let* ((et-locked (- rt1 rt0))
|
||||
(et-lockfree (- rt2 rt1))
|
||||
(ratio (/ et-locked et-lockfree)))
|
||||
(when print
|
||||
(format t "elapsed-times: locked=~d and lockfree=~d ratio=~f~%"
|
||||
et-locked et-lockfree ratio))
|
||||
(setq best (max ratio best))))))))
|
||||
|
||||
(test-util:with-test (:name :lockfree-list-performance)
|
||||
(assert (> (primitive-benchmark)
|
||||
;; should be able to get at least 2x speedup over lock-based code
|
||||
2)))
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
#:checked-compile-capturing-source-paths
|
||||
#:checked-compile-condition-source-paths
|
||||
|
||||
#:runtime #:split-string #:shuffle))
|
||||
#:runtime #:split-string #:integer-sequence #:shuffle))
|
||||
|
||||
(in-package :test-util)
|
||||
|
||||
|
|
@ -695,6 +695,9 @@
|
|||
collect (subseq string begin end)
|
||||
while end))
|
||||
|
||||
(defun integer-sequence (n)
|
||||
(loop for i below n collect i))
|
||||
|
||||
(defun shuffle (sequence)
|
||||
(typecase sequence
|
||||
(list
|
||||
|
|
|
|||
Loading…
Reference in a new issue