Retry optimizing %member-eq

This commit is contained in:
Stas Boukarev 2026-02-17 07:39:11 +03:00
parent d55aaff270
commit 8087d079ce
3 changed files with 163 additions and 123 deletions

View file

@ -451,7 +451,7 @@
(defparameter *list-open-code-limit* 128)
(defun transform-list-item-seek (name item list key test test-not node)
(defun transform-list-item-seek (name item list key test test-not node &optional test-value)
(when (and test test-not)
(abort-ir1-transform "Both ~S and ~S supplied to ~S." :test :test-not name))
;; If TEST is EQL, drop it.
@ -460,130 +460,137 @@
;; Ditto for KEY IDENTITY.
(when (and key (lvar-fun-is key '(identity)))
(setf key nil))
(flet ((test-is (names)
(if test-value
(memq test-value names)
(if test
(lvar-fun-is test names)
(member 'eql names)))))
(when (and (member name '(member assoc rassoc))
(not test-not) ; keep it simple, no other keywords allowed
(not key)
(test-is '(eq eql))
(constant-lvar-p list))
(let ((items (lvar-value list)))
;; spec says MEMBER "Should be prepared to signal an error of type type-error
;; if list is not a proper list." This optimization can't do that.
;; TRY-mumble will figure out based on what function it is trying to transform
;; whether all keys are acceptable.
(when (and (slow-findhash-allowed node)
(proper-list-p items)
(policy node (> jump-table 0)))
(let* ((conditional (if-p (node-dest node)))
(expr (unless (and conditional
(or-eq-transform-p items))
(try-perfect-find/position-map
name
(if conditional ''(t)) ; returned value if present in the mapping
(lvar-type item) items nil node))))
;; Give the user a way to realize that the compiler didn't do what they might have
;; expected it to. What's particularly sad about this is that we had complete
;; freedom to pick any hash function to avoid collisions, so in the case of fixnums,
;; we could have chosen something other than the low 32 bits if that worked better.
;; But I think this is better than feigning ignorance of a problem.
(when (eq expr :fail)
(when (policy node (> jump-table 1))
(compiler-notify 'perfect-hash-generator-failed
:format-control "Hash collisions prevent converting ~S to O(1) search"
:format-arguments (list name)))
(setq expr nil))
(when expr
;; The value delivered to an IF node must be a list because MEMBER and MEMQ
;; are declared in fndb to return a list. If it were just the symbol T,
;; then type inference would get all whacky on you.
(when conditional
(derive-node-type node (specifier-type 'list) :from-scratch t)) ; erase any cons types
(return-from transform-list-item-seek expr))))))
(when (and (member name '(member assoc rassoc))
;; If the test was EQL, we've already changed it to NIL.
(or (not test) (lvar-fun-is test '(eq)))
(not test-not) ; keep it simple, no other keywords allowed
(not key)
(constant-lvar-p list))
(let ((items (lvar-value list)))
;; spec says MEMBER "Should be prepared to signal an error of type type-error
;; if list is not a proper list." This optimization can't do that.
;; TRY-mumble will figure out based on what function it is trying to transform
;; whether all keys are acceptable.
(when (and (slow-findhash-allowed node)
(proper-list-p items)
(policy node (> jump-table 0)))
(let* ((conditional (if-p (node-dest node)))
(expr (unless (and conditional
(or-eq-transform-p items))
(try-perfect-find/position-map
name
(if conditional ''(t)) ; returned value if present in the mapping
(lvar-type item) items nil node))))
;; Give the user a way to realize that the compiler didn't do what they might have
;; expected it to. What's particularly sad about this is that we had complete
;; freedom to pick any hash function to avoid collisions, so in the case of fixnums,
;; we could have chosen something other than the low 32 bits if that worked better.
;; But I think this is better than feigning ignorance of a problem.
(when (eq expr :fail)
(when (policy node (> jump-table 1))
(compiler-notify 'perfect-hash-generator-failed
:format-control "Hash collisions prevent converting ~S to O(1) search"
:format-arguments (list name)))
(setq expr nil))
(when expr
;; The value delivered to an IF node must be a list because MEMBER and MEMQ
;; are declared in fndb to return a list. If it were just the symbol T,
;; then type inference would get all whacky on you.
(when conditional
(derive-node-type node (specifier-type 'list) :from-scratch t)) ; erase any cons types
(return-from transform-list-item-seek expr))))))
;; Key can legally be NIL, but if it's NIL for sure we pretend it's
;; not there at all. If it might be NIL, make up a form to that
;; ensures it is a function.
(multiple-value-bind (key key-form)
(when key
(let ((key-type (lvar-type key))
(null-type (specifier-type 'null)))
(cond ((csubtypep key-type null-type)
(values nil nil))
((types-equal-or-intersect null-type key-type)
(values key '(if key
(%coerce-callable-to-fun key)
#'identity)))
(t
(values key (ensure-lvar-fun-form key 'key))))))
(let ((test-sym 'test)
(test-not-sym 'test-not))
(when (and (or test test-not)
(splice-fun-args (or test test-not) 'complement 1 nil))
(rotatef test test-not)
(rotatef test-sym test-not-sym))
(let* ((c-test (cond ((and test (lvar-fun-is test '(eq)))
(setf test nil)
'eq)
((and (not test) (not test-not))
(when (cond ((or (neq name 'adjoin)
(not key))
(eq-comparable-type-p (lvar-type item)))
(t
(let ((type (lvar-fun-type key t t)))
(when (fun-type-p type)
(eq-comparable-type-p
(single-value-type (fun-type-returns type)))))))
'eq))))
(funs (delete nil (list (when key (list key 'key))
(when test (list test 'test test-sym))
(when test-not (list test-not 'test-not test-not-sym)))))
(target-expr (if key '(%funcall key target) 'target))
(test-expr (cond (test `(%funcall ,test-sym item ,target-expr))
(test-not `(not (%funcall ,test-not-sym item ,target-expr)))
(c-test `(,c-test item ,target-expr))
(t `(eql item ,target-expr)))))
(labels ((open-code (tail)
(when tail
`(if (let ((this ',(car tail)))
,(ecase name
((assoc rassoc)
(let ((cxx (if (eq name 'assoc) 'car 'cdr)))
`(and this (let ((target (,cxx this)))
,test-expr))))
(member
`(let ((target this))
,test-expr))))
',(ecase name
((assoc rassoc) (car tail))
(member tail))
,(open-code (cdr tail)))))
(ensure-fun (args)
(if (eq 'key (second args))
key-form
(ensure-lvar-fun-form (first args) (third args)))))
(let* ((cp (constant-lvar-p list))
(c-list (when cp (lvar-value list))))
(cond ((not (proper-list-p c-list))
(abort-ir1-transform "Argument to ~a is not a proper list." name))
((and cp c-list (member name '(assoc rassoc member))
(policy node (>= speed space))
(not (nthcdr *list-open-code-limit* c-list))
(not (and (producing-fasl-file)
(handler-case (maybe-emit-make-load-forms c-list)
((or compiler-error error) ()
t)))))
`(let ,(mapcar (lambda (fun) `(,(second fun) ,(ensure-fun fun))) funs)
,(open-code c-list)))
((and cp (not c-list))
;; constant nil list
(if (eq name 'adjoin)
'(list item)
nil))
;; Key can legally be NIL, but if it's NIL for sure we pretend it's
;; not there at all. If it might be NIL, make up a form to that
;; ensures it is a function.
(multiple-value-bind (key key-form)
(when key
(let ((key-type (lvar-type key))
(null-type (specifier-type 'null)))
(cond ((csubtypep key-type null-type)
(values nil nil))
((types-equal-or-intersect null-type key-type)
(values key '(if key
(%coerce-callable-to-fun key)
#'identity)))
(t
;; specialized out-of-line version
`(,(specialized-list-seek-function-name name (mapcar #'second funs) c-test)
item list ,@(mapcar #'ensure-fun funs))))))))))
(values key (ensure-lvar-fun-form key 'key))))))
(let ((test-sym 'test)
(test-not-sym 'test-not))
(when (and (or test test-not)
(splice-fun-args (or test test-not) 'complement 1 nil))
(rotatef test test-not)
(rotatef test-sym test-not-sym))
(let* ((c-test (cond ((test-is '(eq))
(setf test nil)
'eq)
((and (not test) (not test-not))
(when (cond ((or (neq name 'adjoin)
(not key))
(eq-comparable-type-p (lvar-type item)))
(t
(let ((type (lvar-fun-type key t t)))
(when (fun-type-p type)
(eq-comparable-type-p
(single-value-type (fun-type-returns type)))))))
'eq))))
(funs (delete nil (list (when key (list key 'key))
(when test (list test 'test test-sym))
(when test-not (list test-not 'test-not test-not-sym)))))
(target-expr (if key '(%funcall key target) 'target))
(test-expr (cond (test `(%funcall ,test-sym item ,target-expr))
(test-not `(not (%funcall ,test-not-sym item ,target-expr)))
(c-test `(,c-test item ,target-expr))
(t `(eql item ,target-expr)))))
(labels ((open-code (tail)
(when tail
`(if (let ((this ',(car tail)))
,(ecase name
((assoc rassoc)
(let ((cxx (if (eq name 'assoc) 'car 'cdr)))
`(and this (let ((target (,cxx this)))
,test-expr))))
(member
`(let ((target this))
,test-expr))))
',(ecase name
((assoc rassoc) (car tail))
(member tail))
,(open-code (cdr tail)))))
(ensure-fun (args)
(if (eq 'key (second args))
key-form
(ensure-lvar-fun-form (first args) (third args)))))
(let* ((cp (constant-lvar-p list))
(c-list (when cp (lvar-value list))))
(cond ((not (proper-list-p c-list))
(abort-ir1-transform "Argument to ~a is not a proper list." name))
((and cp c-list (member name '(assoc rassoc member))
(policy node (>= speed space))
(not (nthcdr *list-open-code-limit* c-list))
(not (and (producing-fasl-file)
(handler-case (maybe-emit-make-load-forms c-list)
((or compiler-error error) ()
t)))))
`(let ,(mapcar (lambda (fun) `(,(second fun) ,(ensure-fun fun))) funs)
,(open-code c-list)))
((and cp (not c-list))
;; constant nil list
(if (eq name 'adjoin)
'(list item)
nil))
(test-value
;; already specialized
(give-up-ir1-transform))
(t
;; specialized out-of-line version
`(,(specialized-list-seek-function-name name (mapcar #'second funs) c-test)
item list ,@(mapcar #'ensure-fun funs)))))))))))
(defun transform-list-pred-seek (name pred list key node)
;; If KEY is IDENTITY, drop it.
@ -705,6 +712,8 @@
(transform-list-item-seek ',name item list key test test-not node))
(deftransform ,basic ((item list) (eq-comparable-type t) * :important nil)
`(,',basic-eq item list))
(deftransform ,basic-eq ((item list) * * :node node :important nil)
(transform-list-item-seek ',name item list nil nil nil node 'eq))
,(unless (eq name 'adjoin) ;; applies KEY to ITEM.
`(deftransform ,basic-key ((item list key) (eq-comparable-type t t) * :important nil)
`(,',basic-key-eq item list key)))

View file

@ -1076,3 +1076,10 @@
(lambda (n)
(aref #("a" "b") n))
simple-string))
(with-test (:name :member-test-eq-reoptimize)
(assert (null
(ctu:ir1-named-calls
`(lambda (l m)
(unless l
(and (member m (or l '(3 5 7)) :test #'eq) t)))))))

View file

@ -1667,5 +1667,29 @@
(#(75E7305A 80D0C591 AC841474 B9B79FF6 C49039A2 E487643D F5B1443E)
"(FUNCTION LIST SIMPLE-VECTOR VECTOR FLOAT DOUBLE-FLOAT SINGLE-FLOAT)"
"((& (^ (>> val 4) (>> val 17)) 7))")
(#(2AA5F8BA 973E9024 9FA987D3 A43E0FD7 A49305EF D396A1B6 E4157BFC)
"(SPECIAL IGNORABLE IGNORE DYNAMIC-EXTENT SB-C::CONSTANT-VALUE SB-C::NO-CONSTRAINTS SB-C::NO-DEBUG)"
"((& (^ (>> val 9) (>> val 30)) 7))")
(#(14 16 18 1A 1C 1E 20 22 26 28 2C)
"(10 11 12 13 14 15 16 17 19 20 22)"
"((let ((tab #a((8) (unsigned-byte 8) 12 13 8 0 0 4 5 2)))
(let ((b (& (>> val 1) #x7)))
(let ((a (>> (<< val 26) 29)))
(^ a (aref tab b))))))")
(#(54D53330 56B428A2 B40D5AD7 DC33E6FC E611D6DF)
"(EQ EQL EQUAL CHAR= CHAR-EQUAL)"
"((& (- val (>> val 10)) 7))")
(#(54D53330 56B428A2 DC33E6FC E611D6DF)
"(EQ EQL EQUAL CHAR=)"
"((& (>> val 1) 3))")
(#(11242EA 9E7E496 C8B4E43 1ED1FDF2 5B5DB13A 70A5F7E1 8EC3EAC9 9F4BDEE8 BC805E5B DC58D005)
"(SB-C:MULTIPLE-CALL SB-C:MULTIPLE-CALL-LOCAL SB-C:MULTIPLE-CALL-NAMED SB-C:STATIC-MULTIPLE-CALL-NAMED SB-C:MULTIPLE-CALL-VARIABLE SB-C:PUSH-VALUES VALUES-LIST SB-C::REVERSE-VALUES-LIST SB-C:%MORE-ARG-VALUES UNALIGNED-DX-CONS)"
"((let ((tab #a((8) (unsigned-byte 8) 8 13 2 0 2 3 5 3)))
(let ((b (& (>> val 11) #x7)))
(let ((a (>> (<< val 31) 29)))
(^ a (aref tab b))))))")
(#(55D24E04 636DB542 B551D38C C2B36623 E76EBABE)
"(STANDARD-CLASS SB-MOP:FUNCALLABLE-STANDARD-CLASS STRUCTURE-CLASS SB-PCL::CONDITION-CLASS SB-PCL::SLOT-CLASS)"
"((& (^ (>> val 4) (>> val 17)) 7))")
)
;; EOF