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:27:07 +0300
Newsgroups gmane.lisp.steel-bank.devel
Message-ID <CAF63=11RscE0XVYtP6MY-cNb1zvbMtVYFLv9vsh-ZL0-PawNTw@mail.gmail.com>
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