mirror of
git://git.code.sf.net/p/sbcl/sbcl
synced 2026-09-10 07:26:40 -04:00
Optimize SSET-INTERSECTION
Impose a stricter upper bound on consing as explained in the comments. Test cases by Gemini
This commit is contained in:
parent
0843efc927
commit
da7d90ca9a
|
|
@ -156,6 +156,7 @@
|
|||
|
||||
;;; Return true if SET contains no elements, false otherwise.
|
||||
(declaim (ftype (sfunction (sset) boolean) sset-empty))
|
||||
(declaim (inline sset-empty))
|
||||
(defun sset-empty (set)
|
||||
(zerop (sset-count set)))
|
||||
|
||||
|
|
@ -178,18 +179,34 @@
|
|||
finally (return modified)))
|
||||
|
||||
(defun sset-intersection (set1 set2)
|
||||
;; If SET2 is _significantly_ smaller than SET1 it would make sense to
|
||||
;; iterate over SET2 looking for the elements that are in SET1.
|
||||
;; However, to do that means consing a new temporary SSET, performing
|
||||
;; the operation, then moving the temporary on top of SET1.
|
||||
;; I don't feel like doing all that.
|
||||
(cond
|
||||
((or (sset-empty set1) (eq set1 set2)) nil)
|
||||
;; Consing can always be bounded by the cardinality of the smaller set,
|
||||
;; we just have to decide whether to collect items to ADJOIN vs DELETE.
|
||||
;; In the situation where SET2 is empty (and SET1 is not, because empty was
|
||||
;; ruled out above), this correctly pick the first of the following two COND
|
||||
;; clauses, doing zero consing.
|
||||
;; When SET2 is no more than half the size of SET1, collecting kept elements
|
||||
;; by scanning SET2 conses at most |SET2| items, whereas scanning SET1
|
||||
;; conses at least |SET1| - |SET2| items.
|
||||
((<= (sset-count set2) (ash (sset-count set1) -1))
|
||||
(let ((to-keep nil))
|
||||
(do-sset-elements (element set2)
|
||||
(when (sset-member element set1)
|
||||
(push element to-keep)))
|
||||
(fill (sset-vector set1) 0)
|
||||
(setf (sset-count set1) 0)
|
||||
;; Since |SET2| < |SET1|, SET1 is guaranteed to shrink, so we always return T.
|
||||
(dolist (element to-keep t)
|
||||
(sset-adjoin element set1))))
|
||||
(t
|
||||
(let ((to-delete nil))
|
||||
(do-sset-elements (element set1)
|
||||
(unless (sset-member element set2)
|
||||
(push element to-delete)))
|
||||
(when to-delete
|
||||
(dolist (element to-delete t)
|
||||
(sset-delete element set1)))))
|
||||
(sset-delete element set1)))))))
|
||||
|
||||
(defun sset-difference (set1 set2)
|
||||
;; If sets are EQ, the algorithms below are either terribly broken (if you pick
|
||||
|
|
|
|||
|
|
@ -178,3 +178,59 @@
|
|||
;; Subtract non-empty from empty
|
||||
(assert (not (sset-difference s2 s1))) ; modified = nil
|
||||
(assert (sset-empty s2)))))
|
||||
|
||||
(with-test (:name :sset-intersection-comprehensive)
|
||||
(let ((elems (loop for i from 1 to 20 collect (make-dummy-test-element i))))
|
||||
;; 1. Small set2, large set1 (Branch 1: set2 <= set1/2)
|
||||
(let ((s1 (make-sset))
|
||||
(s2 (make-sset)))
|
||||
(dolist (e (subseq elems 0 10)) (sset-adjoin e s1)) ; s1 has 10 elements (0..9)
|
||||
(dolist (e (subseq elems 3 6)) (sset-adjoin e s2)) ; s2 has 3 elements (3..5)
|
||||
(assert (sset-intersection s1 s2)) ; modified = t
|
||||
(assert (= (sset-count s1) 3))
|
||||
(assert (sset-member (elt elems 3) s1))
|
||||
(assert (sset-member (elt elems 4) s1))
|
||||
(assert (sset-member (elt elems 5) s1))
|
||||
(assert (not (sset-member (elt elems 0) s1)))
|
||||
(assert (not (sset-member (elt elems 9) s1)))
|
||||
|
||||
;; Intersecting with small disjoint set empties s1
|
||||
(let ((s3 (make-sset)))
|
||||
(dolist (e (subseq elems 15 17)) (sset-adjoin e s3)) ; s3 has 2 elements (15, 16)
|
||||
(assert (sset-intersection s1 s3)) ; modified = t
|
||||
(assert (sset-empty s1))))
|
||||
|
||||
;; 2. Large set2, small set1 (Branch 2: set2 > set1/2)
|
||||
(let ((s1 (make-sset))
|
||||
(s2 (make-sset)))
|
||||
(dolist (e (subseq elems 0 4)) (sset-adjoin e s1)) ; s1 has 4 elements (0..3)
|
||||
(dolist (e (subseq elems 2 12)) (sset-adjoin e s2)) ; s2 has 10 elements (2..11)
|
||||
(assert (sset-intersection s1 s2)) ; modified = t
|
||||
(assert (= (sset-count s1) 2)) ; elements 2 and 3 kept
|
||||
(assert (sset-member (elt elems 2) s1))
|
||||
(assert (sset-member (elt elems 3) s1))
|
||||
(assert (not (sset-member (elt elems 0) s1)))
|
||||
|
||||
;; Intersecting when set2 contains all elements of set1 (unmodified = nil)
|
||||
(let ((s4 (make-sset)))
|
||||
(dolist (e (subseq elems 0 10)) (sset-adjoin e s4)) ; contains all of s1
|
||||
(assert (not (sset-intersection s1 s4))) ; modified = nil
|
||||
(assert (= (sset-count s1) 2))))
|
||||
|
||||
;; 3. (eq set1 set2) returns nil (unmodified)
|
||||
(let ((s1 (make-sset)))
|
||||
(dolist (e (subseq elems 0 5)) (sset-adjoin e s1))
|
||||
(assert (not (sset-intersection s1 s1)))
|
||||
(assert (= (sset-count s1) 5)))
|
||||
|
||||
;; 4. Empty set operations
|
||||
(let ((s1 (make-sset))
|
||||
(s2 (make-sset)))
|
||||
(sset-adjoin (elt elems 0) s1)
|
||||
;; Intersect non-empty with empty (s2 <= s1/2 => returns t, s1 becomes empty)
|
||||
(assert (sset-intersection s1 s2))
|
||||
(assert (sset-empty s1))
|
||||
;; Intersect empty with non-empty (returns nil)
|
||||
(sset-adjoin (elt elems 0) s2)
|
||||
(assert (not (sset-intersection s1 s2)))
|
||||
(assert (sset-empty s1)))))
|
||||
|
|
|
|||
Loading…
Reference in a new issue