1 (shadowing-import 'sb-lockless
::endp
)
2 (import 'sb-int
::(aver fixnump unbound-marker-p
))
3 (import 'sb-sys
:with-pinned-objects
)
4 (import 'sb-kernel
::(generation-of get-lisp-obj-address
))
5 (import 'sb-lockless
::(+hash-nbits
+ %node-next
7 so-head so-bins so-key so-data so-count
9 so-insert so-delete so-find so-find
/string so-maplist
10 make-so-map
/string make-so-set
/string make-so-map
/addr
13 ;;; Make sure no promotions occur so that objects will be movable
14 ;;; throughout these tests.
15 (setf (generation-number-of-gcs-before-promotion 0) 1000000)
17 (defun dummy-node-p (node) (evenp (node-hash node
)))
19 ;;; Show all nodes including dummies.
20 (defun show-list (solist)
21 (let ((node (so-head solist
)))
22 (loop (format t
"~s~%" node
)
23 (when (endp node
) (return))
24 (setq node
(%node-next node
)))))
26 (defun show-bin (solist i
)
27 (let ((node (aref (car (so-bins solist
)) i
))
28 (bin-nbits (- +hash-nbits
+ (cdr (so-bins solist
))))
30 (flet ((bit-string (hash)
31 (let ((s (format nil
" ~v,'0b" +hash-nbits
+ hash
)))
32 (replace s s
:end1 bin-nbits
:start2
1)
33 (setf (char s bin-nbits
) #\.
)
36 ((unbound-marker-p node
)
40 (loop (let ((next (get-next node
)))
41 (when (or (endp next
) (evenp (node-hash next
)))
45 (format t
" ~5d [~2d] = ~a" i count
(bit-string (node-hash node
)))
46 (loop (let ((next (get-next node
)))
47 (when (or (endp next
) (evenp (node-hash next
)))
52 (bit-string (node-hash node
)) (so-key node
))
54 (bit-string (node-hash node
)) (so-key node
)))))
58 (defun show-bins (solist)
59 (let ((bins (car (so-bins solist
)))
60 (bin-nbits (- +hash-nbits
+ (cdr (so-bins solist
))))
64 (aver (= (length bins
) (ash 1 bin-nbits
)))
65 (format t
"Bins (~d total, ~d leading bits):~%"
66 (length bins
) bin-nbits
)
67 (dotimes (i (length bins
))
68 (multiple-value-bind (occupied count
) (show-bin solist i
)
69 (incf n-occupied-bins occupied
)
70 (incf sum-chainlengths count
)
71 (setq max-chainlength
(max count max-chainlength
))))
72 (let ((avg-chainlength (/ sum-chainlengths n-occupied-bins
)))
73 (format t
"~&Total ~D items, avg ~F items/bin~%"
74 (so-count solist
) avg-chainlength
)
75 (values max-chainlength
(float avg-chainlength
)))))
77 (defun print-hashes (solist)
78 (do ((node (%node-next
(so-head solist
)) (%node-next node
)))
80 (format t
"~16x~@[ ~s~]~%"
82 (if (so-key-node-p node
) (type-of (so-key node
))))))
85 ;;; Our SXHASH has _extremely_ bad behavior for the split-order algorithm,
86 ;;; which consumes high bits before low bits. The high bits tend not to get
87 ;;; randomized at all.
88 ;;; Perhaps we should actually try to hash FIXNUMs better for users. Example:
90 (let ((a (+ sb-vm
:dynamic-space-start
(* i
32768))))
91 (format t
"~4d ~x ~v,'0b~%" i a
(or #+64-bit
64 32) (sxhash a
))))
92 0 1000000000 0001000010010001101110100101010000110101100010111010111001001010
93 1 1000008000 0001000010010001101110100101010000110101100000111110111001001010
94 2 1000010000 0001000010010001101110100101010000110101100110110010111001001010
95 3 1000018000 0001000010010001101110100101010000110101100100110110111001001010
96 4 1000020000 0001000010010001101110100101010000110101101010101010111001001010
97 5 1000028000 0001000010010001101110100101010000110101101000101110111001001010
98 6 1000030000 0001000010010001101110100101010000110101101110100010111001001010
99 7 1000038000 0001000010010001101110100101010000110101101100100110111001001010
100 8 1000040000 0001000010010001101110100101010000110101110010011010111001001010
101 9 1000048000 0001000010010001101110100101010000110101110000011110111001001010
102 10 1000050000 0001000010010001101110100101010000110101110110010010111001001010
103 11 1000058000 0001000010010001101110100101010000110101110100010110111001001010
104 12 1000060000 0001000010010001101110100101010000110101111010001010111001001010
105 13 1000068000 0001000010010001101110100101010000110101111000001110111001001010
106 14 1000070000 0001000010010001101110100101010000110101111110000010111001001010
107 15 1000078000 0001000010010001101110100101010000110101111100000110111001001010
111 (defparameter *strings
*
112 (let ((h (make-hash-table :test
'equal
)))
113 (dolist (str (sb-vm:list-allocated-objects
:all
:test
#'simple-string-p
))
114 (setf (gethash (string str
) h
) t
))
115 (loop for str being each hash-key of h collect str
)))
117 (defun fill-table-from (table list
)
118 (dolist (key list table
)
119 (if (hash-table-p table
)
120 (setf (gethash key table
) t
)
121 (so-insert table key t
))))
123 (defun make-threads (nwriters nreaders sem strings-holder writer-fn reader-fn
)
124 (format t
"~&GCing...~%")
126 (format t
"~&Starting test...~%")
128 (loop for i below nwriters
130 (sb-thread:make-thread
131 (lambda (&aux
(ct 0))
132 (sb-thread:wait-on-semaphore sem
)
134 (let ((string (pop (car strings-holder
))))
135 (unless string
(return ct
))
137 (funcall writer-fn string
))))
138 :name
(format nil
"writer ~D" i
)))
139 (loop for i below nreaders
141 (sb-thread:make-thread
142 (lambda (&aux
(found 0))
143 (sb-thread:wait-on-semaphore sem
)
145 ;; just read every string
146 (dolist (str *strings
*)
147 (when (funcall reader-fn str
) (incf found
)))
148 (when (null (car strings-holder
)) (return found
))))
149 :name
(format nil
"reader ~d" i
)))))
151 (defun test-insert-to-synchronized-table (ntrials nwriters nreaders
)
152 (let* ((h (make-hash-table :test
'equal
:synchronized t
))
153 (sem (sb-thread:make-semaphore
))
154 (strings-holder (list *strings
*))
156 (make-threads nwriters nreaders sem strings-holder
157 (lambda (str) (setf (gethash str h
) t
)) ; writer action
158 (lambda (str) (gethash str h
)))) ; reader action
160 (sb-thread:signal-semaphore sem
(+ nwriters nreaders
))
161 (time (dolist (thread threads
)
162 (push (sb-thread:join-thread thread
) results
)))
163 (assert (= (hash-table-count h
) (length *strings
*)))
165 (test-insert-to-synchronized-table (1- ntrials
) nwriters nreaders
)
166 (values h results
))))
168 (defun test-insert-to-lockfree-table (ntrials nwriters nreaders
)
169 (let* ((h (make-so-map/string
))
170 (sem (sb-thread:make-semaphore
))
171 (strings-holder (list *strings
*))
173 (make-threads nwriters nreaders sem strings-holder
174 (lambda (str) (so-insert h str t
))
175 (lambda (str) (so-find/string h str
))))
177 (sb-thread:signal-semaphore sem
(+ nwriters nreaders
))
178 (time (dolist (thread threads
)
179 (push (sb-thread:join-thread thread
) results
)))
180 (assert (= (so-count h
) (length *strings
*)))
181 (dolist (str *strings
*)
182 (assert (so-find/string h str
)))
184 (test-insert-to-lockfree-table (1- ntrials
) nwriters nreaders
)
185 (values h results
))))
187 (defun test-remove-from-synchronized-table (ntrials nthreads
)
188 (let* ((h (fill-table-from (make-hash-table :test
'equal
:synchronized t
)
190 (sem (sb-thread:make-semaphore
))
191 (strings-holder (list *strings
*))
193 (loop for i below nthreads
195 (sb-thread:make-thread
196 (lambda (&aux
(ct 0))
197 (sb-thread:wait-on-semaphore sem
)
199 (let ((string (pop (car strings-holder
))))
200 (unless string
(return ct
))
202 (remhash string h
)))))))
204 (sb-thread:signal-semaphore sem nthreads
)
205 (time (dolist (thread threads
)
206 (push (sb-thread:join-thread thread
) results
)))
207 (assert (= (hash-table-count h
) 0))
209 (test-remove-from-synchronized-table nthreads
(1- ntrials
))
212 (defun test-remove-from-lockfree-table (ntrials nthreads
)
213 (let* ((h (fill-table-from (make-so-map/string
)
215 (sem (sb-thread:make-semaphore
))
216 (strings-holder (list *strings
*))
218 (loop for i below nthreads
220 (sb-thread:make-thread
221 (lambda (&aux
(ct 0))
222 (sb-thread:wait-on-semaphore sem
)
224 (let ((string (pop (car strings-holder
))))
225 (unless string
(return ct
))
227 (so-delete h string
)))))))
229 (sb-thread:signal-semaphore sem nthreads
)
230 (time (dolist (thread threads
)
231 (push (sb-thread:join-thread thread
) results
)))
232 (assert (= (so-count h
) 0))
234 (test-remove-from-lockfree-table nthreads
(1- ntrials
))
238 ;;; - Table starts out with some number of keys (all symbol names)
239 ;;; - One mutator thread removes from the table and notifies a semaphore
240 ;;; each time it has removed one key.
241 ;;; - One mutator thread adds keys and notifies a semaphore.
242 ;;; - The reader asserts that after each semaphore notification,
243 ;;; the table has/doesn't-have the expected key
245 ;; Build a collection of strings to try inserting
246 (defparameter *symbol-names
*
247 (let ((h (make-hash-table :test
'equal
)))
249 (let ((name (string s
)))
250 (when (and (find-if #'upper-case-p name
)
251 (not (find-if #'lower-case-p name
)))
252 (setf (gethash name h
) t
))))
253 (loop for str being each hash-key of h collect str
)))
255 (defun inserter (start-sem msg-sem tbl msgs
)
256 (sb-thread:wait-on-semaphore start-sem
)
257 (dolist (string *symbol-names
*)
258 (let ((key (string-downcase string
)))
260 (atomic-push `(:inserted
,key
) (svref msgs
0))
261 (sb-thread:signal-semaphore msg-sem
)))
262 (atomic-push `(:done
) (svref msgs
0))
263 (sb-thread:signal-semaphore msg-sem
))
264 (defun deleter (start-sem msg-sem tbl msgs
)
265 (sb-thread:wait-on-semaphore start-sem
)
266 (dolist (string *symbol-names
*)
267 (so-delete tbl string
)
268 (atomic-push `(:deleted
,string
) (svref msgs
0))
269 (sb-thread:signal-semaphore msg-sem
))
270 (atomic-push `(:done
) (svref msgs
0))
271 (sb-thread:signal-semaphore msg-sem
))
272 (defun reader (start-sem msg-sem tbl msgs
&aux
(done-count 0))
273 (sb-thread:wait-on-semaphore start-sem
)
275 (sb-thread:wait-on-semaphore msg-sem
)
276 (let ((action (atomic-pop (svref msgs
0))))
279 (let ((key (second action
)))
280 (assert (so-find/string tbl key
))))
282 (let ((key (second action
)))
283 (assert (not (so-find/string tbl key
)))))
285 (when (= (incf done-count
) 2) (return)))))))
288 (let* ((tbl (make-so-set/string
))
289 (start-sem (sb-thread:make-semaphore
))
290 (msg-sem (sb-thread:make-semaphore
))
291 (msgs (make-array 1 :initial-element nil
))
292 (args (list start-sem msg-sem tbl msgs
))
294 (list (sb-thread:make-thread
#'inserter
:arguments args
)
295 (sb-thread:make-thread
#'deleter
:arguments args
)
296 (sb-thread:make-thread
#'reader
:arguments args
))))
297 (dolist (string *symbol-names
*)
298 (so-insert tbl string
))
299 (let ((initial-count (so-count tbl
)))
300 (sb-thread:signal-semaphore start-sem
3)
301 (mapc 'sb-thread
:join-thread threads
)
302 (assert (= (so-count tbl
) initial-count
)))
305 (test-util:with-test
(:name
:basic-functionality
:skipped-on
(not :sb-thread
))
308 ;;; All threads try to insert each key. At most one thread wins,
309 ;;; and the others increment a count associated with the key.
310 ;;; The final count per key should be the number of threads,
311 ;;; and total number of actual insertions performed across threads
312 ;;; should equal the total number of keys.
313 (defun test-insert-same-keys-concurrently (tbl keys
314 &key
(nthreads (floor test-util
:*n-cpus
* 2))
315 ((:delete keys-to-delete
)))
316 (flet ((worker (sem &aux inserted
)
317 (sb-thread:wait-on-semaphore sem
)
320 (let ((delete (atomic-pop (cdr keys-to-delete
))))
322 (so-delete tbl delete
))))
323 (multiple-value-bind (node foundp
) (so-insert tbl key
(list 1))
325 (atomic-incf (car (so-data node
)))
326 (push key inserted
))))
327 ;; Return the list of keys that this worker inserted
329 (let* ((start-sem (sb-thread:make-semaphore
))
330 (args (list start-sem
))
332 (loop repeat nthreads
333 collect
(sb-thread:make-thread
#'worker
:arguments args
))))
334 (sb-thread:signal-semaphore start-sem nthreads
)
335 (let* ((results (mapcar 'sb-thread
:join-thread threads
))
336 (counts (mapcar 'length results
)))
337 (format t
"~&Insertion counts: ~S~%" counts
)
338 (assert (= (reduce #'+ counts
) (length keys
)))
340 (let ((node (so-find tbl key
)))
341 (assert (= (car (so-data node
)) nthreads
))))))))
343 (defun assert-not-found (tbl keys
)
344 ;; no key in keys-to-delete should be in the table
346 (assert (not (so-find tbl k
)))))
348 (test-util:with-test
(:name
:concurrently-insert-same-keys
/string
349 :skipped-on
(not :sb-thread
))
350 (let* ((objects *strings
*)
351 (tbl (make-so-map/string
))
353 (loop for i from
1 to
(length objects
)
354 collect
(let ((key (concatenate 'string
355 ;; avoid colliding with anything
357 (string #+sb-unicode
#\blue_heart
358 #-sb-unicode
(code-char 255))
359 (write-to-string i
))))
360 (so-insert tbl key i
)
362 (test-insert-same-keys-concurrently tbl objects
:delete
(cons nil keys-to-delete
))
363 (assert-not-found tbl keys-to-delete
)
367 ;;; UNEXPECTED-FAILURE :CONCURRENTLY-INSERT-SAME-KEYS/OBJECT
368 ;;; due to UNBOUND-VARIABLE: "The variable X is unbound."
369 ;;; It's talking about the X in the REMOVE-IF lambda.
370 ;;; I tried renaming it to BLAHBLAH and sure enough got
371 ;;; due to UNBOUND-VARIABLE: "The variable BLAHBLAH is unbound."
372 ;;; Based on that I know what the problem was: sb-eval uses a certain magic uninterned
373 ;;; symbol in a LET binding frame to indicate that the variable is special.
374 ;;; To read the binding, it examines the value in the storage location, and if it sees
375 ;;; the magic symbol, it calls SYMBOL-VALUE instead. So sb-eval can't actually represent
376 ;;; a lexical var whose _actual_ _value_ is that magic uninterned symbol.
377 ;;; Instead you'll (potentially) get an error that the variable you referenced is
378 ;;; unbound, unless it really is specially bound also.
379 ;;; Naturally MAP-ALLOCATED-OBJECTS produces that symbol, and so this test can't run,
380 ;;; because we "don't know" what the magic symbol is, since it's uninterned and
381 ;;; therefore can't easily weed it out from the list.
382 ;;; sb-fasteval does not use that same technique to represent special bindings,
383 ;;; and has no problem iterating over all symbols.
384 (test-util:with-test
(:name
:concurrently-insert-same-keys
/object
385 :skipped-on
(or (not :sb-thread
)
386 (and :interpreter
(not :sb-fasteval
))))
388 (remove-if (lambda (x)
389 (not (eql (generation-of x
) sb-vm
:+pseudo-static-generation
+)))
390 (sb-vm:list-allocated-objects
:all
:type sb-vm
:symbol-widetag
)))
391 (tbl (make-so-map/addr
))
393 ;; preload the hashset with some keys that will be deleted
394 ;; concurrently with all the insertions
395 (loop for i from
1 to
(length objects
)
396 do
(so-insert tbl i i
)
398 (test-insert-same-keys-concurrently tbl objects
:delete
(cons nil keys-to-delete
))
399 (assert-not-found tbl keys-to-delete
)))
401 (defun logical-delete (node &aux
(succ (%node-next node
)))
402 (unless (fixnump succ
)
403 (with-pinned-objects (succ)
404 (cas (%node-next node
) succ
(make-marked-ref succ
))))
407 (defparameter *so-map
*
408 (let ((tbl (make-so-map/string
)))
409 (loop for i from
(char-code #\a) to
(char-code #\z
)
410 do
(so-insert tbl
(string (code-char i
)) (char-upcase (code-char i
))))
412 (defparameter *keys-in-table-order
* nil
)
413 (defparameter *deleted-nodes
* nil
)
414 (defparameter *node-addresses
* nil
)
417 (sb-int:collect
((allkeys) (addresses) (deleted))
420 (let ((key (so-key node
)))
422 (when (evenp (incf x
))
424 (logical-delete node
))
425 (addresses (get-lisp-obj-address node
))))
427 (setf *keys-in-table-order
* (allkeys)
428 *deleted-nodes
* (deleted)
429 *node-addresses
* (addresses))))
432 ;;; Most of the node addresses should have changed,
433 ;;; but the list should still be fully intact.
434 (test-util:with-test
(:name
:solist-integrity
)
435 (let ((node (so-head *so-map
*))
437 (addresses *node-addresses
*)
438 (keys *keys-in-table-order
*))
440 (cond ((endp node
) (return))
441 ((dummy-node-p node
) ; skip
442 (setq node
(get-next node
)))
444 (assert (eq (so-key node
) (pop keys
)))
445 (unless (= (get-lisp-obj-address node
) (pop addresses
))
447 (multiple-value-bind (next next-bits
) (get-next node
)
448 (let ((node-deletedp (fixnump next-bits
))
449 (found (so-find *so-map
* (so-key node
))))
450 (assert (eq (not (null (find node
*deleted-nodes
*)))
454 (assert (eq found node
))))
457 (assert (>= addr-change
22)))) ; seems about right
459 ;;; SO-MAPLIST does not include deleted nodes.
460 ;;; Nodes marked for deletion should still be marked after GC
461 ;;; (though it should be possible to modify GC to finish deletion)
462 (test-util:with-test
(:name
:solist-mid-deletion
)
466 (member string
*deleted-nodes
* :key
#'so-key
:test
#'string
=))
467 *keys-in-table-order
*)))
468 (so-maplist (lambda (node)
469 (assert (string= (so-key node
) (pop present-keys
))))
472 (defvar *example-objects
*
474 (remove-if (lambda (x) (/= (sb-kernel:generation-of x
) sb-vm
:+pseudo-static-generation
+))
475 (sb-vm:list-allocated-objects
:dynamic
:type sb-vm
:simple-base-string-widetag
))
476 (sb-vm:list-allocated-objects
:read-only
:type sb-vm
:simple-base-string-widetag
))
479 (test-util:with-test
(:name
:c-find-in-solist
)
480 (let ((set (sb-lockless:make-so-set
/addr
)) )
481 (dolist (x *example-objects
*)
482 (sb-lockless:so-insert set x
))
483 (assert (not (sb-lockless:c-so-find
/addr set
'random
)))
484 (dolist (x *example-objects
*)
485 (let ((node (sb-lockless:c-so-find
/addr set x
)))
487 (assert (eq (sb-lockless:so-key node
) x
))))))
489 (test-util:with-test
(:name
:solist-2-phase-insert
)
490 (let ((set (sb-lockless:make-so-set
/addr
))
491 (example-objects *example-objects
*)
494 ;; This example is artificial. The real usage would allocate one object and perform
495 ;; both insert phases in the following pattern:
496 ;; begin pseudo-atomic
497 ;; allocate split-order node 'n'
498 ;; allocate off-heap large unboxed object
499 ;; phase1 insert node 'n' pointing to large-object
502 ;; If GC occurs just after the pseudo-atomic section, it is possible to test each stack word
503 ;; as being a conservative pointer to an off-heap object based on its presence in the table.
504 (dolist (object example-objects
)
505 (let ((node (sb-lockless::%make-so-set-node
0 0)))
507 (sb-lockless::%so-eq-set-phase1-insert set node object
)))
508 ;; It has no bearing on currectness that the table count is understated and that
509 ;; the number of bins may be too few prior to running the second step.
510 ;; This is evident from the loop below which shows that each example-object can be found.
512 (sb-lockless::%so-eq-set-phase2-insert set node
)
513 ;; delete some keys at random. This could occur only after the node is fully inserted.
514 (when (zerop (random 10))
515 (sb-lockless:so-delete set
(sb-lockless:so-key node
))
518 (loop for object in example-objects
519 count
(sb-lockless:so-find set object
))))
520 (assert (= table-count
(- 1000 n-deleted
)))))
521 ;; Finally the table count should be correct
522 (assert (= (sb-lockless::so-count set
) (- 1000 n-deleted
)))
527 Small test
: 20k keys
, 8 writers
, 2 readers
528 ==========================================
529 * (test-insert-to-synchronized-table 1 8 2)
533 0.048 seconds of real time
534 0.261340 seconds of total run time
(0.091142 user
, 0.170198 system
)
536 123,504,864 processor cycles
537 4,029,824 bytes consed
538 #<HASH-TABLE
:TEST EQUAL
:COUNT
24141 {10010A2803
}>
539 * (test-insert-to-lockfree-table 1 8 2)
543 0.011 seconds of real time
544 0.096867 seconds of total run time
(0.096867 user
, 0.000000 system
)
546 33,070,828 processor cycles
547 4,924,544 bytes consed
548 #<SPLIT-ORDERED-LIST
24141 keys
, 16384 bins
{1003CA0373
}>
551 Large test
: 96k keys
, 8 writers
, 4 readers
552 ==========================================
553 * (test-insert-to-synchronized-table 1 8 4)
557 0.344 seconds of real time
558 2.180249 seconds of total run time
(0.689017 user
, 1.491232 system
)
560 963,461,936 processor cycles
561 15,657,312 bytes consed
562 #<HASH-TABLE
:TEST EQUAL
:COUNT
95949 {10010A56B3
}>
563 * (test-insert-to-lockfree-table 1 8 4)
567 0.047 seconds of real time
568 0.453098 seconds of total run time
(0.453098 user
, 0.000000 system
)
570 136,186,892 processor cycles
571 12,143,696 bytes consed
572 #<SPLIT-ORDERED-LIST
95949 keys
, 32768 bins
{10010E2C83
}>
575 Huge test
: 122k keys
, 8 writers
, 10 readers
576 ============================================
577 * (test-insert-to-synchronized-table 1 8 10)
581 1.007 seconds of real time
582 10.534241 seconds of total run time
(2.623577 user
, 7.910664 system
)
584 2,815,228,188 processor cycles
585 29,103,872 bytes consed
586 * (test-insert-to-lockfree-table 1 8 10)
590 0.068 seconds of real time
591 1.069891 seconds of total run time
(1.069891 user
, 0.000000 system
)
593 187,631,304 processor cycles
594 22,077,904 bytes consed