This is likely a veeeery old question, but strangely I hadn't given much thought about it.
Do people use improper lists such as (a b c . d)
with more than 2 elements? I don't remember ever seeing such a use case.
If not, wouldn't it be possible to just disallow constructing improper lists apart from pairs? That would make list?
a constant-time check, and would avoid the need for many user-defined procedures to have to worry about improper lists.
sorawee
February 21, 2022, 11:06am
2
One boring answer is that it’s used for representing function definitions with a rest argument
> (eval '(define (f x . y) (append y (list x))))
> (f 1 2 3)
'(2 3 1)
1 Like
Right—I forgot that one indeed. I wish this was represented with a keyword instead
(define (f x #:rest y) (append y (list x)))
.
(edit) Oh but then that wouldn't work with apply
so well I guess?
Just to make it explicit: the time used for list?
is O(1) today.
For BC it is O(1) and for CS it is amortized O(1).
In BC pairs have an PAIR_IS_LIST
flag.
The beginning of list?
for BC looks like this:
int scheme_is_list(Scheme_Object *obj1)
{
Scheme_Object *obj2;
int flags;
if (SCHEME_PAIRP(obj1)) {
flags = SCHEME_PAIR_FLAGS(obj1);
if (flags & PAIR_FLAG_MASK) {
if (flags & PAIR_IS_LIST)
return 1;
else
return 0;
}
} else if (SCHEME_NULLP(obj1))
return 1;
else
return 0;
...
For CS you need to check the rumble layer:
;; Make `list?` an amoritized constant-time operation, which is
;; possible because `set-cdr!` is not exposed. Make it constant-time
;; by caching the result for an N-item list at the N/2-tail pair,
;; where a second request will cache at the N/4-tail pair, etc.
;; Detect cycles using the same `slow` tortoise that is used for
;; caching.
;;
;; To reduce the overhead of checking the hash table, only
;; start using it after the first `CHECK-AFTER-LEN` pairs.
;; Then, check only after `CHECK-SKIP-N` pairs --- and record
;; a sequence of `CHECK-SKIP-N`+1 results so one will hit when
;; checking every `CHECK-SKIP-N` pairs.
(define (list? v) (list-assuming-immutable? v))
(define (append-n l n l2)
(cond
[(fx= 0 n) l2]
[else (cons (car l) (append-n (cdr l) (fx1- n) l2))]))
This file has been truncated. show original
5 Likes
sorawee
February 21, 2022, 12:03pm
5
@Laurent.O : #:rest
could work, but this would affect user-introducing keyword arguments — you would need to reserve #:rest
to avoid ambiguity — which is fine, but kinda weird.
@soegaard : the actual logic for amortized constant time is no longer in the rumble layer (and the comments there probably should be removed). It's become a primitive in Chez Scheme: https://github.com/racket/racket/commit/120082f3f922cf3a38756a8a3023ccaed6dfe894 .
7 Likes
@soegaard For a little bit more context, amortized O(1) can still be significant:
#lang racket
(define N 100000000)
(let ()
(define l (time (make-list N 'a)))
(time (let loop ([l l])
(if (empty? l) ; implies `list?`
#true
(loop (rest l))))))
(let ()
(define l (time (make-list N 'a)))
(time (let loop ([l l])
(if (null? l)
#true
(loop (cdr l))))))
cpu time: 4719 real time: 4719 gc time: 4565 <-- make-list
cpu time: 8929 real time: 8929 gc time: 2
#t
cpu time: 4273 real time: 4273 gc time: 4119 <-- make-list
cpu time: 178 real time: 178 gc time: 0
#t
Possibly this is testing more than list?
, but I suspect it's the biggest offender anyway.
1 Like
sorawee
February 21, 2022, 1:14pm
7
https://github.com/racket/racket/issues/3109 might be related?
I think list?
probably won’t get any faster, but it might be possible to eliminate many calls of list?
entirely with a smarter compiler optimization.
Related indeed. In the program above, ideally, list?
would be checked only once outside the loop. And indeed this would be fast:
(let ()
(define l (time (make-list N 'a)))
(time (list? l))
(time (let loop ([l l])
(if (null? l)
#true
(loop (cdr l))))))
cpu time: 4384 real time: 4385 gc time: 4224
cpu time: 286 real time: 286 gc time: 0
cpu time: 171 real time: 171 gc time: 0
#t
The compiler 'merely' needs to prove that l
always remains a list after first testing the full list (which should follow from make-list
in any case).
1 Like
I am surprised the difference is as large as it is.
(I am assuming you are using a version newer than the one sorawee points to).
Maybe the contract of rest
and first
should be changed to pair?
instead of list?
.