Re: [Sbcl-bugs] [BUG] WITH-OPEN-FILE registers redundant auto-close finalizer (~31% of OPEN allocation)

Stas Boukarev <[email protected]> Wed, 11 Mar 2026 19:44:30 +0300
Newsgroups gmane.lisp.steel-bank.devel
Message-ID <CAF63=12+DzJVv0vjXd6-K2DykGReHhgt9jG+cOfqVXcOLgSysA@mail.gmail.com>
Solved in a different way. Thanks for the idea.


On Wed, Mar 11, 2026 at 6:41 PM John Mallery <[email protected]> wrote:
>
> SBCL version: 2.6.1 (also affects current git HEAD)
> Platform: all
>
> DESCRIPTION
>
> Every call to OPEN registers an auto-close finalizer on the fd-stream
> via FINALIZE (which calls SB-LOCKLESS:SO-INSERT to insert into the
> finalizer hash table).  This allocates a closure and a hash table
> node.  When OPEN is called from WITH-OPEN-FILE, the finalizer is
> redundant because the UNWIND-PROTECT in WITH-OPEN-FILE guarantees
> CLOSE is called on both normal and abnormal exit.
>
> Profiling with sb-sprof in :alloc mode shows SB-LOCKLESS:SO-INSERT
> accounting for ~31% of OPEN's total allocation.  In a web server
> that opens/closes files thousands of times per second, this is a
> significant source of GC pressure.
>
> HOW TO REPRODUCE
>
>  (require :sb-sprof)
>
>  (let ((path (namestring *load-truename*)))  ; any existing file
>    (dotimes (i 50) (close (open path)))      ; warm buffer pool
>
>    ;; Profile allocation in OPEN
>    (sb-sprof:start-profiling :mode :alloc :sample-interval 1)
>    (dotimes (i 5000) (close (open path)))
>    (sb-sprof:stop-profiling)
>    (sb-sprof:report :type :flat :max 10))
>
>  ;; Output shows SB-LOCKLESS:SO-INSERT at ~31% of samples
>
> FIX
>
> Two files changed: src/code/fd-stream.lisp and src/code/macros.lisp.
>
> 1. Add a dynamic variable *SUPPRESS-STREAM-AUTO-CLOSE* that gates
>   the finalizer registration in MAKE-FD-STREAM:
>
> --- a/src/code/fd-stream.lisp
> +++ b/src/code/fd-stream.lisp
> @@ -2475,6 +2475,12 @@
> ;;;
> ;;; NAME is used to identify the stream when printed.
> ;;;
> +;;; When true, MAKE-FD-STREAM skips the auto-close finalizer registration.
> +;;; Bound by WITH-OPEN-FILE, where the UNWIND-PROTECT guarantees CLOSE,
> +;;; making the finalizer redundant.  Eliminates ~31% of OPEN allocation
> +;;; (finalizer closure + SO-INSERT hash table node).
> +(defvar *suppress-stream-auto-close* nil)
> +
> ;;; If SERVE-EVENTS is true, SERVE-EVENT machinery is used to
> ;;; handle blocking IO on the stream.
> (defun make-fd-stream (fd
> @@ -2544,7 +2550,7 @@
>       (set-fd-stream-routines stream element-type ...)
> -      (when auto-close
> +      (when (and auto-close (not *suppress-stream-auto-close*))
>         (finalize stream
>                   (lambda ()
>                     (sb-unix:unix-close fd)
>
> 2. Modify WITH-OPEN-FILE to bind *SUPPRESS-STREAM-AUTO-CLOSE* to T.
>   The stream variable is initialized to NIL and set inside the
>   UNWIND-PROTECT scope so the suppression is active during OPEN:
>
> --- a/src/code/macros.lisp
> +++ b/src/code/macros.lisp
> @@ -1437,12 +1437,18 @@
>   (multiple-value-bind (forms decls) (parse-body body nil)
>     (let ((abortp (gensym)))
> -      `(let ((,stream (open ,filespec ,@options))
> -             (,abortp t))
> +      ;; Suppress the auto-close finalizer -- UNWIND-PROTECT guarantees
> +      ;; CLOSE, making the finalizer redundant.
> +      `(let ((,stream)
> +             (,abortp t)
> +             (sb-impl::*suppress-stream-auto-close* t))
>          ,@decls
>          (unwind-protect
>               (multiple-value-prog1
> -                  (progn ,@forms)
> +                  (progn
> +                    (setf ,stream (open ,filespec ,@options))
> +                    ,@forms)
>                 (setq ,abortp nil))
>            (when ,stream
>              (close ,stream :abort ,abortp)))))))
>
> SAFETY
>
> The auto-close finalizer is ONLY suppressed within WITH-OPEN-FILE,
> where the UNWIND-PROTECT guarantees CLOSE.  Bare OPEN calls outside
> WITH-OPEN-FILE still register the finalizer as a safety net.
>
> If WITH-OPEN-FILE's OPEN succeeds but the thread is destroyed before
> UNWIND-PROTECT runs (e.g., SB-THREAD:TERMINATE-THREAD), the fd would
> leak.  However, this is already the case for any resource acquired
> inside an UNWIND-PROTECT -- thread termination can prevent cleanup
> forms from running regardless of finalizers.
>
> IMPACT
>
> Any I/O-intensive application that opens files via WITH-OPEN-FILE
> benefits.  Web servers, compilers, and build systems are typical
> examples.  The fix eliminates ~31% of OPEN's allocation with zero
> behavioral change for correct programs.
>
>
> _______________________________________________
> 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