forked from acl2/acl2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
hons-raw.lisp
4192 lines (3661 loc) · 173 KB
/
hons-raw.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
; ACL2 Version 8.1 -- A Computational Logic for Applicative Common Lisp
; Copyright (C) 2018, Regents of the University of Texas
; This version of ACL2 is a descendent of ACL2 Version 1.9, Copyright
; (C) 1997 Computational Logic, Inc. See the documentation topic NOTE-2-0.
; This program is free software; you can redistribute it and/or modify
; it under the terms of the LICENSE file distributed with ACL2.
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; LICENSE for more details.
; Regarding authorship of ACL2 in general:
; Written by: Matt Kaufmann and J Strother Moore
; email: [email protected] and [email protected]
; Department of Computer Science
; University of Texas at Austin
; Austin, TX 78712 U.S.A.
; hons-raw.lisp -- Raw lisp implementation of hash cons and fast alists. Note
; that the memoization and watch functionality previously provided by this file
; have been moved into memoize-raw.lisp. This file has undergone a number of
; updates and changes since its original creation in about 2005.
; The original version of this file was contributed by Bob Boyer and Warren
; A. Hunt, Jr. The design of this system of Hash CONS, function memoization,
; and fast association lists (applicative hash tables) was initially
; implemented by Boyer and Hunt. The code was subsequently improved by Boyer
; and Hunt, and also by Sol Swords, Jared Davis, and Matt Kaufmann. This file
; is now maintained by the ACL2 authors (see above).
; This code is well commented and the comments have been contributed and
; improved by all of the authors named above. However, comments referring to
; "I" or "my" are from Jared, as is the token "BOZO" which he uses to leave
; a note about a wart or modification to consider.
; Despite the comments here, we recommend reading the user-level documentation
; for HONS-AND-MEMOIZATION before diving into this code.
(in-package "ACL2")
; NOTES ABOUT HL-HONS
;
; The "HL-" prefix was introduced when Jared Davis revised the Hons code, and
; "HL" originally meant "Hons Library." The revision included splitting the
; Hons code from the function memoization code, and took place in early 2010.
; We will now use the term to refer to the new Hons implementation that is
; found below. It might be helpful to read the Essay on Hons Spaces before
; proceeding.
; Some changes made in HL-Hons, as opposed to the old Hons system, include:
;
; - We combine all of the special variables used by the Hons code into an
; explicit Hons-space structure.
;
; - We no longer separately track the length of sbits. This change appears
; to incur an overhead of 3.35 seconds in a 10-billion iteration loop, but
; gives us fewer invariants to maintain and makes Ctrl+C safety easier.
;
; - We have a new static honsing scheme where every normed object has an
; address, and NIL-HT, CDR-HT, and CDR-HT-EQL aren't needed when static
; honsing is used.
;
; - Since normed strings are EQ comparable, we now place cons pairs whose
; cdrs are strings into the CDR-HT hash table instead of the CDR-HT-EQL
; hash table in classic honsing.
;
; - Previously, fast alists were essentially implemented as flex alists.
; (See the essay about flex alists for an introduction to these
; structures.) Now we always create a hash table instead. This slightly
; simplifies the code and results in trivially fewer runtime type checks in
; HONS-GET and HONS-ACONS. Our implementation does still use flex alists
; under the hood to implement classic honsing (specifically, in the CDR-HT
; and CDR-HT-EQL fields of a hons space), where we can imagine often
; finding cdrs for which we don't have at least 18 separate cars. But fast
; alists are much more targeted; if ACL2 users are building fast alists, it
; seems very likely that they know they are doing something big (or
; probably big) -- otherwise they wouldn't be bothering with fast alists.
; ESSAY ON CTRL+C SAFETY
;
; The HL-Hons implementation involves certain impure operations. Sometimes, at
; intermediate points during a sequence of updates, the invariants on a Hons
; Space are violated. This is dangerous because a user can interrupt at any
; time using 'Ctrl+C', leaving the system in an inconsistent state.
;
; We have tried to avoid sequences of updates that violate invariants. In the
; rare cases where this isn't possible, we need to protect the sequence of
; updates with 'without-interrupts'. We assume that SETF itself does not
; suffer from this kind of problem and that the Lisp implementation should
; ensure that, e.g., (SETF (GETHASH ...)) does not leave a hash table in an
; internally inconsistent state.
; CROSS-LISP COMPATIBILITY WRAPPERS
;
; As groundwork toward porting the static honsing scheme to other Lisps that
; might be able to support it, we have added these wrappers for various ccl::
; functionality.
(defun hl-mht-fn (&key (test 'eql)
(size '60)
(rehash-size '1.5)
(rehash-threshold '0.7)
(weak 'nil)
(shared 'nil)
(lock-free 'nil))
; See hl-mht. Here we have a special hack: if :shared is :default then we
; leave it to the underlying Lisp (in particular CCL) to decide about :shared
; and :lock-free.
(declare (ignorable shared weak lock-free))
(cond ((eq shared :default)
(make-hash-table :test test
:size size
:rehash-size rehash-size
:rehash-threshold rehash-threshold
#+ccl :weak #+sbcl :weakness
#+(or ccl sbcl) weak
))
(t
(make-hash-table :test test
:size size
:rehash-size rehash-size
:rehash-threshold rehash-threshold
#+ccl :weak #+sbcl :weakness
#+(or ccl sbcl) weak
#+ccl :shared #+ccl shared
#+ccl :lock-free #+ccl lock-free
))))
#+allegro
(declaim (type hash-table *allegro-hl-hash-table-size-ht*))
#+allegro
(defvar *allegro-hl-hash-table-size-ht*
; See the comment about this hash table in hl-mht.
(make-hash-table))
(defmacro hl-mht (&rest args)
; This macro is a wrapper for hl-mht-fn, which in turn is a wrapper for
; make-hash-table. But here we have a special hack: if :shared is :default
; then we leave it to the underlying Lisp (in particular CCL) to decide about
; :shared and :lock-free.
; Because of our approach to threading, we generally don't need our hash tables
; to be protected by locks. HL-MHT is essentially like make-hash-table, but on
; CCL creates hash tables that aren't shared between threads, which may result
; in slightly faster updates.
; In Allegro CL 9.0 (and perhaps other versions), make-hash-table causes
; creation of a hash table of a somewhat larger size than is specified by the
; :size argument, which can cause fast-alist-fork to create hash tables of
; ever-increasing size when this is not necessary. The following example
; illustrates this problem, which was first observed in community book
; books/parsers/earley/earley-parser.lisp.
; (defconst *end* :end)
;
; (defn my-fast-alist-fork (a)
; (let ((ans (fast-alist-fork a *end*)))
; (prog2$ (fast-alist-free a)
; ans)))
;
; (defun my-fast-alist3 (n ans)
; (declare (type (integer 0 *) n))
; (cond ((zp n) ans)
; (t (my-fast-alist3 (1- n)
; (my-fast-alist-fork
; (hons-acons (mod n 10) (* 10 n) ans))))))
;
; (trace! (hl-mht-fn :native t :exit t))
;
; (time$ (my-fast-alist3 33 *end*))
; Our solution is to use a hash table, *allegro-hl-hash-table-size-ht*, to map
; the actual size of a hash table, h, to the :size argument of the call of
; hl-mht that created h. Then, when we want to create a hash table of a given
; :size, new-size-arg, we look in *allegro-hl-hash-table-size-ht* to check
; whether a previous call of hl-mht created a hash table of that size using
; some :size, old-size-arg, and if so, then we call make-hash-table with :size
; old-size-arg instead of new-size-arg. (Note that we don't give this special
; treatment in the case that hl-mht is called without an explicit :size; but
; that seems harmless.)
#-allegro
`(hl-mht-fn ,@args)
#+allegro
(let ((tail (and (keyword-value-listp args)
(assoc-keyword :size args))))
(cond (tail
(let ((size-arg-var (gensym))
(old-size-arg-var (gensym)))
`(let* ((,size-arg-var ,(cadr tail))
(,old-size-arg-var
(gethash ,size-arg-var *allegro-hl-hash-table-size-ht*)))
(cond (,old-size-arg-var
(hl-mht-fn :size ,old-size-arg-var
,@(remove-keyword :size args)))
(t (let ((ht (hl-mht-fn ,@args)))
(setf (gethash (hash-table-size ht)
*allegro-hl-hash-table-size-ht*)
,size-arg-var)
ht))))))
(t `(hl-mht-fn ,@args)))))
; ESSAY ON STATIC CONSES
; In CCL, one can use (ccl::static-cons a b) in place of (cons a b) to create a
; cons that will not be moved by the garbage collector.
;
; Unlike an ordinary cons, every static cons has a unique index, which is a
; fixnum, and which may be obtained with (ccl::%staticp x). Per Gary Byers,
; this index will be forever fixed, even after garbage collection, even after
; saving an image.
;
; Even more interesting, the cons itself can be recovered from its index using
; ccl::%static-inverse-cons. Given the index of a static cons, such as
; produced by ccl::%staticp, %static-inverse-cons produces the corresponding
; static cons. Given an invalid index (such as the index of a cons that has
; been garbage collected), %static-inverse-cons produces NIL. Hence, this
; gives us a way to tell if a cons has been garbage collected, and lets us
; implement a clever scheme for "washing" honses.
;
; We now write wrappers for static-cons, %staticp, and %static-inverse-cons, to
; make it easier to plug in alternative implementations in other Lisps.
#+static-hons
(defmacro hl-static-cons (a b)
#+gcl `(cons ,a ,b)
#-gcl `(ccl::static-cons ,a ,b))
#+static-hons
(defmacro hl-staticp (x)
; This function always returns a fixnum.
; CCL::%STATICP always returns a fixnum or nil, as per Gary Byers email June
; 16, 2014. That email also confirmed that if the value is not nil after a
; garbage collection, then the value is unchanged from before the garbage
; collection; and also, that the value remains unchanged after saving an image.
; Indeed, this function returns a fixnum (see also above) if x is a static
; cons. More may be true, as follows, but we don't count on it: in mid-2014,
; at least, we see that for CCL, the value returned is 128 for the first static
; cons and is incremented by 1 for each additional static cons -- and after a
; garbage collection, this repeats except that values for remaining static
; conses are skipped.
; See also *hl-hspace-sbits-default-size*.
#+gcl
; Camm Maguire tells us that conses are always 16-byte aligned in 64-bit GCL
; and 8-byte aligned in 32-bit GCL.
`(the fixnum
(ash (the fixnum (si::address ,x))
#+x86_64 -4
#-x86_64 -3))
#-gcl
`(ccl::%staticp ,x))
#+static-hons
(defmacro hl-static-inverse-cons (x)
#+gcl `(si::static-inverse-cons (the fixnum (ash (the fixnum ,x)
#+x86_64 4
#-x86_64 3)))
#-gcl `(ccl::%static-inverse-cons ,x))
#+static-hons
(defmacro hl-machine-hash (x)
; NOT A FUNCTION. Note that (EQUAL (HL-MACHINE-HASH X) (HL-MACHINE-HASH X)) is
; not necessarily true, because objects may be moved during garbage collection
; in CCL.
;
; We use the machine address of an object to compute a hash code within [0,
; 2^20). We obtain a good distribution on 64-bit CCL, but we have not tried
; this on 32-bit CCL.
;
; We use the CCL primitive ccl::strip-tag-to-fixnum because it always returns a
; fixnum, while ccl::%address-of (which returns the machine address) might
; conceivably not do so, according to email 6/2014 from Gary Byers. He
; explains that CCL uses ccl::strip-tag-to-fixnum "to derive a fixnum from its
; argument's address (it effectively does something like a right shift by 4
; bits....".
;
; For CCL, we right-shift the address by five places because smaller shifts led
; to worse distributions. (GCL wart: We haven't really thought this efficiency
; issue through for GCL.) Gary Byers has informed us (email, 6/16/2014) that
; in 64-bit CCL, memory-allocated objects (like conses) are 16-byte aligned,
; and the bottom 4 bits (all 0) are replaced by tag bits (where those tag bits
; are always 3 for a cons). So shifting by 4 bits might seem to suffice, but
; we got a significantly better distribution shifting by 5 bits (see discussion
; of experiments below, "However, in addition to fast execution speed....").
;
; It should be easy to change this from 2^20 to other powers of 2. We think
; 2^20 is a good number, since a 2^20 element array seems to require about 8 MB
; of memory, e.g., our whole cache (which consists of two such arrays; see
; "Implementation 2" of Cache Tables, below) will take 16 MB.
;
`(the fixnum (ash (the fixnum
(logand #x1FFFFF
#+gcl (ash (si::address ,x) -4)
#+ccl (ccl::strip-tag-to-fixnum ,x)
#-(or gcl ccl)
(error "~s is not implemented in this Lisp."
'hl-machine-hash)
#-(or gcl ccl)
0))
-1)))
; ----------------------------------------------------------------------
;
; CACHE TABLES
;
; ----------------------------------------------------------------------
; A Cache Table is a relatively simple data structure that can be used to
; (partially) memoize the results of a computation. Cache tables are used by
; the hons implementation, but are otherwise independent from the rest of hons.
; We therefore introduce them here, up front.
;
; The operations of a Cache Table are as follows:
;
; (HL-CACHE-GET KEY TABLE) Returns (MV PRESENT-P VAL)
; (HL-CACHE-SET KEY VAL TABLE) Destructively modifies TABLE
;
; In many ways, a Cache Table is similar to an EQ hash table, but there are
; also some important differences. Unlike an ordinary hash table, a Cache
; Table may "forget" some of its bindings at any time. This allows us to
; ensure that the Cache Table does not grow beyond a fixed size.
;
; Cache Tables are not thread safe.
;
; We have two implementations of Cache Tables.
;
; Implementation 1. For Lisps other than 64-bit CCL (#-static-hons).
;
; A Cache Table is essentially an ordinary hash table, along with a separate
; field that tracks its count.
;
; It is almost an invariant that this count should be equal to the
; HASH-TABLE-COUNT of the hash table, but we do NOT rely upon this for
; soundness and this property might occasionally be violated due to
; interrupts. In such cases, we ensure that the count is always more than
; the HASH-TABLE-COUNT of the hash table. (The only negative consequence of
; this is that the table may be cleared more frequently.)
;
; The basic scheme is as follows. Whenever hl-cache-set is called, if the
; count exceeds our desired threshold, we clear the hash table before we
; continue. The obvious disadvantage of this is that we may throw away
; results that may be useful. But the advantage is that we ensure that the
; cache table does not grow. On one benchmark, this approach was about
; 17% slower than letting the hash table grow without restriction (notably
; ignoring all garbage collection costs), but it lowered the memory usage
; from 2.8 GB to 92 MB.
;
; We have considered improving the performance by experimenting with the
; size of its cache. A larger cache means less frequent clearing but
; requires more memory. We also looked into more smartly clearing out the
; cache. One idea was to throw away only half of the entries "at random" by
; just allowing the maphash order to govern whether we throw out one entry
; or another. But when this was slow, we discovered that, at least on CCL,
; iterating over a hash table is fairly expensive and requires the keys and
; values of the hash table to be copied into a list. For a 500k cache,
; "clearing" half the entries required us to allocate 6 MB, and ruined
; almost all memory savings we had hoped for. Hence, we just use ordinary
; clrhash.
;
; Implementation 2. For 64-bit CCL (#+static-hons) we use a better scheme.
;
; A Cache Table contains two arrays, KEYDATA and VALDATA. These arrays
; associate "hash codes" (array indices) to keys and values, respectively.
; We could have used a single array with (key . val) pairs as its entries,
; but using two separate arrays allows us to implement hl-cache-set with no
; consing.
;
; These hash codes are based upon the actual machine address of KEY, and
; hence (1) may easily collide and (2) are not reliable across garbage
; collections. To give a sensible semantics, in hl-cache-get, we must
; explicitly check that this hash code has the proper KEY.
;
; Our hashing function, hl-machine-hash, performs quite well. According to
; a rough test, it takes only about the same time as about 11 fixnum
; additions. Just below is a log of our tests (on a 2010 MacBook Pro).
;
; ? (defun f (x)
; (let ((acc 0))
; (declare (type fixnum acc))
; (loop for i fixnum from 1 to 1000000000
; do
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (car x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (cdr x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (car x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (cdr x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (car x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (cdr x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (car x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (cdr x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (car x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (cdr x)))))
; (setq acc (the fixnum (+ (the fixnum acc) (the fixnum (car x)))))
; )
; acc))
; F
; ? (defun g (x)
; (let ((acc 0))
; (declare (type fixnum acc))
; (loop for i fixnum from 1 to 1000000000
; do
; (setq acc (the fixnum (hl-machine-hash (car x)))))
; acc))
; G
; ? (time (f '(1 . 2)))
; (F '(1 . 2))
; took 3,398,028 microseconds (3.398028 seconds) to run.
; During that period, and with 4 available CPU cores,
; 3,394,546 microseconds (3.394546 seconds) were spent in user mode
; 1,643 microseconds (0.001643 seconds) were spent in system mode
; 16000000000
; ? (time (g '((1 . 2))))
; (G '((1 . 2)))
; took 3,387,291 microseconds (3.387291 seconds) to run.
; During that period, and with 4 available CPU cores,
; 3,384,752 microseconds (3.384752 seconds) were spent in user mode
; 1,501 microseconds (0.001501 seconds) were spent in system mode
; 90258
; ?
;
; However, in addition to fast execution speed, we want this function to
; produce a good distribution so that we may hash on its result. We have
; hard-coded in a size of 2^20 for our data arrays, but it would not be very
; hard to change this. To determine how well it distributes addresses, we
; computed the hash codes for a list of 2^24 objects, which is more than the
; 2^20 hash codes that we have made available. We found that every hash
; code was used 15, 16, or 17 times, a nearly perfect distribution! (Of
; course, when this is used on actual data produced by a program, we do not
; expect the results to be so good.) Here is some basic testing code:
;
; (include-book "std/osets/sort" :dir :system)
; (include-book "defsort/duplicated-members" :dir :system)
;
; (defparameter *hashes* nil)
;
; (let* ((tries (expt 2 24))
; (hashes (time (loop for i fixnum from 1 to tries collect
; (hl-machine-hash (cons 1 2)))))
; (nhashes (time (len (set::mergesort hashes)))))
; (setq *hashes* hashes)
; (format t "Got ~:D entries for ~:D tries.~%" nhashes tries))
;
; (defparameter *dupes* (hons-duplicity-alist *hashes*))
; (set::mergesort (strip-cdrs *dupes*))
;
; Result: "Got 1,048,576 entries for 16,777,216 tries."
;
; Result: (set::mergesort (strip-cdrs *dupes*)) is (15 16 17).
;
; Specifically, execution of
;
; (let ((ans nil))
; (loop for pair in *dupes* as n = (cdr pair)
; do (setq ans (put-assoc n (1+ (or (cdr (assoc n ans)) 0)) ans))
; finally (return ans)))
;
; returned:
;
; ((15 . 93880) (16 . 860816) (17 . 93880))
;
; However, when we only shifted four bits of the machine address using
;
; `(logand #xFFFFF (hl-machine-address-of ,x))
;
; for the body of hl-machine-hash, we got a much worse distribution that
; used only half the hash codes, perhaps because of how CCL implements
; ccl::%address-of on Darwin (as these experiments were on a Mac).
;
; ((31 . 88652) (33 . 96844) (32 . 334696) (30 . 4096))
;
; We also tried shifting six bits of the address using
;
; `(ash (the fixnum (logand #x3FFFFF (hl-machine-address-of ,x))) -2)
;
; for the body of hl-machine-hash, but that distribution was also not as
; good:
;
; ((16 . 198089) (14 . 422171) (18 . 426266) (17 . 2) (12 . 2048))
; BOZO: Implicitly, our cache table has NIL bound to NIL; this might not
; be appropriate for "memoizing" other applications.
#-static-hons
(defconstant hl-cache-table-size
;; Size of the cache table
400000)
#-static-hons
(defconstant hl-cache-table-cutoff
;; How full a cache table (e.g., the norm-cache of a hons space) can get
;; before we clear it. This also becomes the maximum possible value of
;; the count field (see hl-cache-set). Must be a fixnum.
(let ((ans (floor (* 0.75 hl-cache-table-size))))
(cond ((> ans most-positive-fixnum)
(error "Hl-cache-table-cutoff is too big to be a fixnum!"))
(t ans))))
(defstruct hl-cache
#+static-hons
(keydata (make-array (expt 2 20) :initial-element nil) :type simple-vector)
#+static-hons
(valdata (make-array (expt 2 20) :initial-element nil) :type simple-vector)
#-static-hons
(table (hl-mht :test #'eq :size hl-cache-table-size) :type hash-table)
#-static-hons
(count 0
; Function hl-cache-set guarantees that count is a fixnum.
:type fixnum))
(defun hl-cache-set (key val cache)
(declare (type hl-cache cache))
#+static-hons
(let ((keydata (hl-cache-keydata cache))
(valdata (hl-cache-valdata cache))
(code (hl-machine-hash key)))
(without-interrupts
(setf (svref keydata (the fixnum code)) key)
(setf (svref valdata (the fixnum code)) val)))
#-static-hons
(let ((table (hl-cache-table cache))
(count (hl-cache-count cache)))
;; This is a funny ordering which is meant to ensure the count exceeds or
;; agrees with (hash-table-count table), even in the face of interrupts.
(cond ((>= (the fixnum count)
(the fixnum hl-cache-table-cutoff))
(clrhash table)
; We set count to one, not zero, because we're about to add an element.
(setf (hl-cache-count cache) 1))
(t (setf (hl-cache-count cache)
; Since count < hl-cache-table-cutoff, (+ 1 count) is guaranteed to be a
; fixnum.
(the fixnum (+ 1 (the fixnum count))))))
(setf (gethash key table) val)))
(defun hl-cache-get (key cache)
; (HL-CACHE-GET KEY CACHE) --> (MV PRESENT-P VAL)
;
; WARNING: Key must not be nil, at least in the case #+static-hons (see
; below).
;
; Parallelism wart: Note that this isn't thread-safe. If we want a truly
; multithreaded hons, we'll need to think about how to protect access to the
; cache.
(declare (type hl-cache cache))
#+static-hons
(let* ((keydata (hl-cache-keydata cache))
(code (hl-machine-hash key))
(elem-key (svref keydata (the fixnum code))))
(if (eq elem-key key) ; assumption: not :initial-element of keydata, nil
(let* ((valdata (hl-cache-valdata cache))
(elem-val (svref valdata (the fixnum code))))
(mv t elem-val))
(mv nil nil)))
#-static-hons
(let* ((table (hl-cache-table cache))
(val (gethash key table)))
(if val
(mv t val)
(mv nil nil))))
(defun hl-cache-clear (cache)
(declare (type hl-cache cache))
#+static-hons
(let ((keydata (hl-cache-keydata cache))
(valdata (hl-cache-valdata cache)))
(loop for i fixnum below (expt 2 20) do
(setf (svref keydata i) nil)
(setf (svref valdata i) nil)))
#-static-hons
(progn
;; This ordering ensures count >= (hash-table-count table) even in
;; the face of interrupts
(clrhash (hl-cache-table cache))
(setf (hl-cache-count cache) 0)))
; ESSAY ON HONS SPACES
;
; The 'ACL2 Objects' are described in the ACL2 function bad-lisp-objectp;
; essentially they are certain "good" symbols, characters, strings, and
; numbers, recursively closed under consing. Note that live stobjs are not
; ACL2 Objects under this definition.
;
; The 'Hons Spaces' are fairly complex structures, introduced with the
; defstruct for hl-hspace, which must satisfy certain invariants. At any time
; there may be many active Hons Spaces, but separate threads may never access
; the same Hons Space! This restriction is intended to minimize the need to
; lock while accessing Hons Spaces.
;
; Aside. Sharable Hons Spaces might have some advantages. They might
; result in lower overall memory usage and reduce the need to re-hons data
; in multiple threads. They might also be a better fit for ACL2(p)'s
; parallelism routines. But acquiring locks might slow honsing in
; single-threaded code and make our code more complex. Parallelism wart: We
; may investigate this later.
;
;
; Fundamental Operations.
;
; A Hons Space provides two fundamental operations.
;
; First, given any ACL2 Object, X, and any Hons Space HS, we must be able to
; determine whether X is 'normed' with respect to HS. The fundamental
; invariant of normed objects is that if A and B are both normed with respect
; to HS, then (EQUAL A B) holds iff (EQL A B).
;
; Second, given any ACL2 Object, X, and any Hons Space, HS, we must be able to
; 'norm' X to obtain an ACL2 Object that is EQUAL to X and which is normed with
; respect to HS. Note that norming is 'impure' and destructively modifies HS.
; This modification is really an extension: any previously normed object will
; still be normed, but previously non-normed objects may now also be normed.
;
;
; Tracking Normed Objects.
;
; To support these operations, a Hons Space contains certain hash tables and
; arrays that record which ACL2 Objects are currently regarded as normed.
;
; Symbols, characters, and numbers. These objects automatically and trivially
; satisfy the fundamental invariant of normed objects. We therefore regard all
; symbols, characters, and numbers as normed with respect to any Hons Space,
; and nothing needs to be done to track whether particular symbols, characters,
; or numbers are normed.
;
; Strings. Within each Hons Space, we may choose some particular string, X, as
; the normed version of all strings that are equal to X. We record this choice
; in the STR-HT field of the Hons Space, which is an EQUAL hash table. The
; details of what we record in the STR-HT actually depend on whether 'classic
; honsing' or 'static honsing' is being used. See below.
;
; Conses. Like strings, there are EQUAL conses which are not EQL. We could
; account for this by setting up another equal hash table, as we did for
; strings, but EQUAL hashing of conses can be very slow. More efficient
; schemes are possible because we insist upon two reasonable invariants:
;
; INVARIANT C1. The car of a normed cons must be normed.
; INVARIANT C2. The cdr of a normed cons must be normed.
;
; Using these invariants, we have developed two schemes for tracking which
; conses are normed. One approach, called classic-honsing, makes use of only
; ordinary Common Lisp functions. The second approach, static-honsing, is
; higher performance but requires features that are specific to Clozure Common
; Lisp.
; ESSAY ON CLASSIC HONSING
;
; Prerequisite: see the essay on hons spaces.
;
; Classic Honsing is a scheme for tracking normed conses that can be used in
; any Lisp. Here, every normed cons is recorded in one of three hash tables.
; In particular, whenever X = (A . B) is normed, then X will be found in
; either:
;
; NIL-HT, when B is NIL,
; CDR-HT, when B is a non-NIL symbol, a cons, or a string, or
; CDR-HT-EQL otherwise.
;
; The NIL-HT binds A to X whenever X = (A . NIL) is a normed cons. Thanks to
; Invariant C1, which assures us that A will be normed, we only need to use an
; EQL hash table for NIL-HT.
;
; For other conses, we basically implement a two-level hashing scheme. To
; determine if a cons is normed, we first look up its CDR in the CDR-HT or
; CDR-HT-EQL, depending on its type. Both of these tables bind B to the set of
; all normed X such that X = (A . B) for any A. These sets are represented as
; 'flex alists', defined later in this file. So, once we have found the proper
; set for B, we look through it and see whether there is a normed cons in it
; with A as its CAR.
;
; We use the CDR-HT (an EQ hash table) for objects whose CDRs are
; EQ-comparable, and the CDR-HT-EQL (an EQL hash table) for the rest. We may
; use CDR-HT for both strings and conses since, by Invariant C2, we know that
; the CDR is normed and hence pointer equality suffices.
;
; The only other thing to mention is strings. In the classic honsing scheme,
; the STR-HT simply associates each string to its normed version. That is, a
; string X is normed exactly when (eq X (gethash X STR-HT)). It is
; straightforward to norm a string X: a STR-HT lookup (using EQUAL) tells us
; whether a normed version of X exists and, if so, what it is. Otherwise, when
; no normed version of X exists, we effectively 'norm' X by extending the
; STR-HT by binding X to itself.
;
; Taken all together, the STR-HT, NIL-HT, CDR-HT, and CDR-HT-EQL completely
; determine which ACL2 objects are normed in the classic honsing scheme.
; ESSAY ON STATIC HONSING
;
; Prerequisite: see the Essay on Hons Spaces and the Essay on Static Conses.
;
; Static Honsing is a scheme for tracking normed conses that can be used only
; in Clozure Common Lisp (CCL).
;
; Static Honsing is an alternative to classic honsing that exploits static
; conses for greater efficiency. Static conses are conses with several
; interesting properties: to each static cons there corresponds a unique
; natural number ``index''; it is possible to ``invert'' the index to retrieve
; the static cons; each static cons has a fixnum ``machine address.'' Static
; conses were implemented by Gary Byers and references to ``Gary'' below are to
; him.
;
; Here, only static conses can be considered normed, and SBITS is a bit-array
; that records which static conses are currently normed. That is, suppose X is
; a static cons and let I be the index of X. Then X is considered normed
; exactly when the Ith bit of SBITS is 1. This is a very fast way to determine
; if a cons is normed!
;
;
; Addresses for Normed Objects.
;
; To support hons construction, we also need to be able to do the following:
; given two normed objects A and B, find the normed version of (A . B) or
; determine that none exists.
;
; Toward this goal, we first develop a reliable 'address' for every normed
; object; this address has nothing to do with machine (X86, PowerPC, or other)
; addresses. To begin, we statically assign addresses to NIL, T, and certain
; small integers. In particular (see also historic notes below):
;
; Characters are given addresses 0-255, corresponding to their codes.
; NIL and T are given addresses 256 and 257, respectively.
; Integers in [-2^14, 2^23] are given the subsequent addresses
; successively starting at -2^14 (address 258).
;
; All other objects are dynamically assigned addresses. In particular, suppose
; that BASE is the start of the dynamically-allocated range. Then,
;
; The address of a static cons, C, is Index(C) + BASE, where Index(C) is the
; index returned by HL-STATICP.
;
; For any other atom, X, we construct an associated static cons, say X_C,
; and then use Index(X_C) + BASE as the address of X.
;
; This scheme gives us a way to generate a unique, reliable address for every
; ACL2 Object we encounter. These addresses start small and increase as more
; static conses are created.
;
; We record the association of these "other atoms" to their corresponding
; static conses in different ways, depending upon their types:
;
; For symbols, the static cons is stored in the 'hl-static-address property
; for the symbol. This results in something a little bizarre: symbol
; addresses are implicitly shared across all Hons Spaces, and so we must take
; care to ensure that our address-allocation code is thread safe.
;
; For strings, the STR-HT binds each string X to the pair XC = (NX
; . TRUE-ADDR), where NX is the normed version of X and XC is a static cons
; whose address is TRUE-ADDR (see hl-hspace-norm-atom).
;
; For any other atoms, the Hons Space includes OTHER-HT, an EQL hash table
; that associates each atom X with X_C, the static cons for X.
;
; In the future, we might want to think about the size of BASE. Gary might be
; be able to extend static cons indices so that they start well after 128,
; perhaps eliminating the need to add BASE when computing the addresses for
; static conses. On the other hand, it's probably just a matter of who is
; doing the addition, and our current scheme gives us good control over the
; range.
;
;
; Address-Based Hashing.
;
; Given the addresses of two normed objects, A and B, the function
; hl-addr-combine generates a unique integer that can be used as a hash key.
;
; Each Hons Space includes ADDR-HT, an EQL hash table that associates these
; keys to the normed conses to which they correspond. In other words, suppose
; C = (A . B) is a normed cons, and KEY is the key generated for the addresses
; of A and B. Then ADDR-HT associates KEY to C.
;
; Hence, assuming A and B are normed, we can determine whether a normed cons of
; the form (A . B) exists by simply generating the proper KEY and then checking
; whether ADDR-HT includes an entry for this key.
;
; We maintain the invariant that SBITS and ADDR-HT must correspond as described
; above. That is, if (aref SBITS idx) = 1 then idx is the index of a static
; cons that is a key of ADDR-HT; and if C is a key of ADDR-HT, then C is a
; static cons such that (aref SBITS idx) = 1, where idx is the index of C.
; Default Sizes. The user can always call hl-hons-resize to get bigger tables,
; but we still want good defaults. These sizes are used in the structures that
; follow. We try to avoid using any size less than 100. A hash table of size
; 100 is pretty small and should be cheap to allocate, and making a hash table
; under that size is possibly not a very sensible thing to do, as it may
; quickly require resizing, etc.
(defparameter *hl-hspace-str-ht-default-size*
;; Usually there aren't too many strings, so let's start out small and just
;; let this grow if the user happens to be using a lot of strings.
1000)
(defparameter *hl-ctables-nil-ht-default-size*
;; Classic honsing, table for honses (a . b) where b is NIL. We might see
;; relatively many of these conses because NIL often terminates list, so
;; start this off with a moderately large size.
5000)
(defparameter *hl-ctables-cdr-ht-default-size*
;; Classic honsing, main table for almost all honses, so make it large.
100000)
(defparameter *hl-ctables-cdr-ht-eql-default-size*
;; Classic honsing, table for honses (a . b) where b is, e.g., a character or
;; number. Not very common, so don't make this too large to start with.
1000)
(defparameter *hl-hspace-addr-ht-default-size*
;; Static honsing, main table for honses, needs to be large
150000)
(defparameter *hl-hspace-sbits-default-size*
;; Static honsing sbits array; pretty cheap. It seems pretty sensible to
;; just match the size of the address table, given how indices appear to be
;; generated for static conses (see hl-staticp), at least for CCL. But we
;; believe everything would work correctly even if this were a tiny value
;; like 100. For example, in hl-hspace-truly-static-honsp we do an explicit
;; bounds check before accessing sbits[i], and in hl-hspace-hons-normed we do
;; an explicit bounds check and call hl-hspace-grow-sbits if there isn't
;; enough room before setting sbits[i] = 1.
*hl-hspace-addr-ht-default-size*)
(defparameter *hl-hspace-other-ht-default-size*
;; Static honsing addresses for unusual atoms, probably doesn't need to be
;; very large.
1000)
(defparameter *hl-hspace-fal-ht-default-size*
;; Fast alists table, probably there aren't going to be many fast alists
;; floating around so this doesn't need to be too large.
1000)
(defparameter *hl-hspace-persist-ht-default-size*
;; For persistent honses. Hardly anyone ever uses these so let's not
;; allocate very many by default.
100)
; Foreshadowing: We provide a means, hl-hspace-hons-clear, to wipe out all
; honses -- except the mechanism protects fast alists and any hons marked as
; ``persistent'' in the hl-hspace-persist-ht table.
#-static-hons
(defstruct hl-ctables
; CTABLES STRUCTURE. This is only used in classic honsing. We aggregate
; together the NIL-HT, CDR-HT, and CDR-HT-EQL fields so that we can clear them
; out all at once in hl-hspace-hons-clear, for Ctrl+C safety.
(nil-ht (hl-mht :test #'eql :size *hl-ctables-nil-ht-default-size*)
:type hash-table)
(cdr-ht (hl-mht :test #'eq :size *hl-ctables-cdr-ht-default-size*)
:type hash-table)
(cdr-ht-eql (hl-mht :test #'eql :size *hl-ctables-cdr-ht-eql-default-size*)
:type hash-table))
(defun hl-initialize-faltable-table (fal-ht-size)
; Create the initial TABLE for the FALTABLE. See the Essay on Fast Alists,
; below, for more details.
;
; [Sol]: Note (Sol): The non-lock-free hashing algorithm in CCL [for keyword
; argument :lock-free = nil in make-hash-table; also see
; http://trac.clozure.com/ccl/wiki/Internals/LockFreeHashTables] seems to have
; some bad behavior when remhashes are mixed in with puthashes in certain
; patterns. One of these is noted below by Jared in the "Truly disgusting
; hack" note. Another is that when a table grows to the threshold where it
; should be resized, it is instead rehashed in place if it contains any deleted
; elements -- so if you grow up to 99% of capacity and then repeatedly insert
; and delete elements, you're likely to spend a lot of time rehashing without
; growing the table.
;
; [Jared]: Truly disgusting hack. As of Clozure Common Lisp revision 14519, in
; the non lock-free version of 'remhash', there is a special case: deleting the
; last remaining element from a hash table triggers a linear walk of the hash
; table, where every element in the vector is overwritten with the
; free-hash-marker. This is devastating when there is exactly one active fast
; alist: every "hons-acons" and "fast-alist-free" operation requires a linear
; walk over the TABLE. This took me two whole days to figure out. To ensure
; that nobody else is bitten by it, and that I am not bitten by it again, here
; I ensure that the TABLE always has at least one fast alist within it. This
; alist is unreachable from any ordinary ACL2 code so it should be quite hard
; to free it. BOZO: Jared thinks it would be reasonable to see if this problem
; is fixed in CCL, in which case this code could be simplified.
(let ((table (hl-mht :test #'eq :size (max 100 fal-ht-size)
; We could specify :lock-free t, but perhaps it's faster with default nil.
:weak :key)))
#+ccl
;; This isn't necessary with lock-free, but doesn't hurt. Note that T is
;; always honsed, so sentinel is a valid fast-alist. I give this a
;; sensible name since it can appear in the (fast-alist-summary).
(let* ((entry (cons t t))
(sentinel-al (cons entry 'special-builtin-fal))
(sentinel-ht (hl-mht :test #'eql)))
(setf (gethash t sentinel-ht) entry)
(setf (gethash sentinel-al table) sentinel-ht))
table))
(defstruct hl-falslot
; FAST-ALIST CACHE SLOT. See the Essay on Fast Alists, below, for more
; details.
(key nil) ;; The alist being bound, or NIL for empty slots
(val nil) ;; Its backing hash table
(uniquep t :type boolean) ;; Flag for memory consistency
; Invariant 1. If KEY is non-nil, then it is a valid fast alist.
;
; Invariant 2. If KEY is non-nil, VAL is the appropriate backing hash table
; for this KEY (i.e., it is not some old/stale hash table or some newer/updated
; hash table.)
;
; Invariant 3. If UNIQUEP is true, then KEY is not bound in the main TABLE,
; i.e., it exists only in this slot.
;
; Invariant 4. No slots ever have the same KEY unless it is NIL.
)
(defstruct (hl-faltable (:constructor hl-faltable-init-raw))
; FAST-ALIST TABLE STRUCTURE. See the Essay on Fast Alists, below, for more
; details.
;
; This is essentially just an association from alists to backing hash tables.
; We previously made the associations an EQ hash table for alists to their
; backing hash tables. And logically, that's all the HL-FALTABLE is.
;
; But, as a tweak, we add a small cache in front. This cache allows us to
; avoid hashing in the very common cases where we're growing up a new hash
; table or repeatedly doing lookups in just a couple of hash tables. The cache
; consists of fields slot1 and slot2, together with a Boolean, eject1, that
; tells us whether to eject slot1 (as opposed to slot2) when making a new cache
; entry.
;
; BOZO consider using CCL weak "populations" to make the slots weak like the
; table.
(slot1 (make-hl-falslot) :type hl-falslot)
(slot2 (make-hl-falslot) :type hl-falslot)
(eject1 nil :type boolean) ;; want to eject slot1 on cache miss?
(table (hl-initialize-faltable-table *hl-hspace-fal-ht-default-size*)
:type hash-table))
(defun hl-faltable-init (&key (size *hl-hspace-fal-ht-default-size*))
(hl-faltable-init-raw :table (hl-initialize-faltable-table size)))
(defstruct (hl-hspace (:constructor hl-hspace-init-raw))
; HONS SPACE STRUCTURE. See the above essays on hons spaces, classic honsing,
; and static honsing above to understand this structure.
(str-ht (hl-mht :test #'equal :size *hl-hspace-str-ht-default-size*)
:type hash-table)
;; Classic Honsing
#-static-hons
(ctables (make-hl-ctables)
:type hl-ctables)
;; Static Honsing
#+static-hons
(addr-ht (hl-mht :test #'eql :size *hl-hspace-addr-ht-default-size*)
:type hash-table)