Re: [Sbcl-bugs] [BUG] FORMAT tokenize-control-string cache too small (128 entries, thrashes in large applications)

Stas Boukarev <[email protected]> Wed, 11 Mar 2026 18:58:07 +0300
Newsgroups gmane.lisp.steel-bank.devel
Message-ID <CAF63=13zdP4Hn8BBOK8NrLAcWJ0trFK9=BuYWK=OPXS2ye6cfA@mail.gmail.com>
Huge RAM? In this economy?

On Wed, Mar 11, 2026 at 6:57 PM John Mallery <[email protected]> wrote:
>
> What do you make of this in the era huge ram?
>
> > On Mar 11, 2026, at 16:51, Stas Boukarev <[email protected]> wrote:
> >
> > Larger caches are not free. And your 8KB math is only for the vector
> > holding strings, not the strings themselves.
> >
> > On Wed, Mar 11, 2026 at 6:50 PM John C Mallery <[email protected]> wrote:
> >>
> >> Well, this is not an argument to avoid making the format cache larger.
> >>
> >>> On Mar 11, 2026, at 16:47, Stas Boukarev <[email protected]> wrote:
> >>>
> >>> But if you are after performance why are you calling format with
> >>> unknown strings. Maybe use FORMATTER?
> >>>
> >>> On Wed, Mar 11, 2026 at 6:45 PM John C Mallery <[email protected]> wrote:
> >>>>
> >>>> I don’t know about SBCL, but we have a fast-format function which does exactly that.  We don’t handle, however, complex directives.  In any case, this bug in SBCL is fixed by using a large hash table to cache the string.
> >>>>
> >>>>> On Mar 11, 2026, at 16:27, Stas Boukarev <[email protected]> wrote:
> >>>>>
> >>>>> But why are these format strings not processed at compile time?
> >>>>>
> >>>>>
> >>>>> On Wed, Mar 11, 2026 at 6:22 PM John C Mallery <[email protected]> wrote:
> >>>>>>
> >>>>>> SBCL version: 2.6.1 (also affects current git HEAD)
> >>>>>> Platform: all
> >>>>>>
> >>>>>> DESCRIPTION
> >>>>>>
> >>>>>> The FORMAT control string tokenizer cache (tokenize-control-string)
> >>>>>> uses :hash-bits 7, giving a 128-entry 2-way set-associative cache.
> >>>>>> In large applications with many distinct format strings, the cache
> >>>>>> fills completely and thrashes -- every collision re-tokenizes the
> >>>>>> control string, allocating fresh tokenized representations.
> >>>>>>
> >>>>>> In CL-HTTP (a web server with ~200 distinct format strings in its
> >>>>>> codebase), the cache is 128/128 full.  Profiling under load shows
> >>>>>> tokenize-control-string accounting for 7.0% of all allocation during
> >>>>>> HTTP request serving (~28K req/s).
> >>>>>>
> >>>>>> HOW TO REPRODUCE
> >>>>>>
> >>>>>> ;; Create enough distinct format strings to fill the 128-entry cache
> >>>>>> (let ((strings (loop for i below 200
> >>>>>>                    collect (format nil "test-~D: ~~A ~~D" i))))
> >>>>>> ;; Warm the cache
> >>>>>> (dolist (s strings) (format nil s "x" 1))
> >>>>>> ;; Measure -- cache is now thrashing
> >>>>>> (let ((before (sb-ext:get-bytes-consed)))
> >>>>>>   (dotimes (j 100)
> >>>>>>     (dolist (s strings)
> >>>>>>       (format nil s "x" 1)))
> >>>>>>   (format t "~,1F bytes/format call~%"
> >>>>>>           (/ (float (- (sb-ext:get-bytes-consed) before))
> >>>>>>              (* 100.0 (length strings))))))
> >>>>>>
> >>>>>> ;; With :hash-bits 7 (128 entries): ~200+ bytes/call (re-tokenizing)
> >>>>>> ;; With :hash-bits 10 (1024 entries): ~0 bytes/call (cache hits)
> >>>>>>
> >>>>>> FIX
> >>>>>>
> >>>>>> 1-line change in src/code/format.lisp.  Increase :hash-bits from 7
> >>>>>> to 10 (1024 entries instead of 128):
> >>>>>>
> >>>>>> --- a/src/code/format.lisp
> >>>>>> +++ b/src/code/format.lisp
> >>>>>> @@ -23,7 +23,7 @@
> >>>>>> #-sb-xc-host
> >>>>>> (defun-cached (tokenize-control-string
> >>>>>>             :memoizer memoize
> >>>>>> -               :hash-bits 7
> >>>>>> +               :hash-bits 10
> >>>>>>             :hash-function (lambda (string)
> >>>>>>                              (ash (get-lisp-obj-address string)
> >>>>>>                                   #.(- sb-vm:n-lowtag-bits))))
> >>>>>>
> >>>>>> COST
> >>>>>>
> >>>>>> The cache vector grows from 128 to 1024 entries (simple-vector).
> >>>>>> On 64-bit, this is ~8KB of memory -- negligible for any application
> >>>>>> large enough to have 128+ distinct format strings.  The cache is
> >>>>>> allocated lazily on first use, so small programs pay nothing.
> >>>>>>
> >>>>>> The defun-cached assert already allows hash-bits up to 12:
> >>>>>> (assert (typep hash-bits '(integer 5 12)))
> >>>>>>
> >>>>>> IMPACT
> >>>>>>
> >>>>>> Any application with more than ~64 active format strings (the
> >>>>>> effective capacity of a 128-entry 2-way cache) will see thrashing.
> >>>>>> Web servers, GUI applications, and compilers commonly exceed this.
> >>>>>>
> >>>>>>
> >>>>>> _______________________________________________
> >>>>>> Sbcl-bugs mailing list
> >>>>>> [email protected]
> >>>>>> https://lists.sourceforge.net/lists/listinfo/sbcl-bugs
> >>>>
> >>
>


_______________________________________________
Sbcl-devel mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/sbcl-devel