On Wed, 18 Feb 2026, Ludovic Courtès <[email protected]> wrote:
> Hello,
>
> Olivier Dion <[email protected]> skribis:
>
>> I guess this is internal to how libgc work.  From my understanding, and
>> debugging sessions, the finalized objects is marked when the callback is
>> called.  Thus, the sweep (after the finalizers?) will keep it.  I guess
>> this makes some sens since finalizers can re-introduce "dead" objects.
>> For obscure reasons, the objects is never finalized again, yet it
>> should.
>>
>> My theory is, after the call to the finalizer, a flag is unset from the
>> object (done by libgc), thus removing our finalizer that we've just
>> installed.  The object is then simply garbaged collect as usual but we
>> just don't see it.  Would be interesting to track that object with a
>> watchpoint in GDB to see that in action.
>
> OK.

So I tried a different approach where instead of using a single atomic
pointer, I use two of them and store only one in a root node.  Then,
when the dangling pointer is finalized, I swap it with the one in the
root node.  Kind of like a pivot point.  See diff at the end.

I do get two calls to the finalizer, from the two distinct pointers, but
no more.  So I really think that finalized pointers can simply never be
finalized after, even if they are resurrected by the finalizer itself.
Even thought, I look into the internal code of libgc, I don't see why
finalizer could not be called twice ..

Also just to show you the performance difference between the previous
patch and allocating a new atomic pointer in the finalizer (I bump the
loop from 10 to 50 just to put more stress):

# A: Baseline (no vacuum)
Benchmark 1: ./meta/guile ./reproducer.scm
  Time (mean ± σ):     215.9 ms ±   2.2 ms    [User: 512.3 ms, System: 60.0 ms]
  Range (min … max):   213.0 ms … 221.4 ms    13 runs

# B: New atomic pointer everytime (single line trivial fix)
Benchmark 1: ./meta/guile ./reproducer.scm
  Time (mean ± σ):     265.2 ms ±  24.0 ms    [User: 571.7 ms, System: 66.5 ms]
  Range (min … max):   234.5 ms … 301.9 ms    11 runs

# C: The proposed patch that re-work the finalizer module
Benchmark 1: ./meta/guile ./reproducer.scm
  Time (mean ± σ):     229.5 ms ±   4.7 ms    [User: 636.8 ms, System: 64.9 ms]
  Range (min … max):   224.0 ms … 238.7 ms    12 runs


And a perf-diff(1) of A, B and C:

# Data files:
#  [0] perf.data (Baseline)
#  [1] perf.data.trivial
#  [2] perf.data.rework
#
# Baseline/0  Delta Abs/1  Delta Abs/2  Shared Object          Symbol
# ..........  ...........  ...........  .....................  
.............................................
#
      67.08%       -2.71%       +6.47%  libgc.so.1.5.4         [.] 
0x0000000000008bf3
       1.86%       +1.22%       +0.72%  libc.so.6              [.] 
pthread_mutex_trylock@@GLIBC_2.34
                   +1.02%               [JIT] tid 17129        [.] 
0x00007f24059c9085
       1.17%       +0.95%       -0.14%  libgc.so.1.5.4         [.] GC_base
       0.06%       +0.63%       -0.02%  libguile-3.0.so.1.7.1  [.] 
resize_set.lto_priv.0
       0.75%       -0.60%       -0.43%  libgc.so.1.5.4         [.] 
GC_generic_malloc_many
       2.57%       -0.51%       -1.16%  libgc.so.1.5.4         [.] GC_is_marked
       0.88%       +0.50%       -0.23%  libguile-3.0.so.1.7.1  [.] 
give_to_poor.lto_priv.0

So more time in GC, yet better result?

[...]


pivot.diff:
--8<---------------cut here---------------start------------->8---
diff --git a/libguile/finalizers.c b/libguile/finalizers.c
index f6e67d5d7..8d4f3e412 100644
--- a/libguile/finalizers.c
+++ b/libguile/finalizers.c
@@ -358,12 +358,17 @@ scm_i_finalizer_pre_fork (void)
 static void
 async_gc_finalizer (void *ptr, void *data)
 {
-  void **obj = ptr;
-  void (*callback) (void) = obj[0];
+  struct async_finalizer *af;
 
-  callback ();
+  af = *(void **)ptr;
 
-  scm_i_set_finalizer (ptr, async_gc_finalizer, data);
+  printf("async_gc_finalizer\n");
+
+  scm_i_set_finalizer (ptr, async_gc_finalizer, NULL);
+  scm_i_set_finalizer (af->safe_pointer, async_gc_finalizer, NULL);
+
+  af->callback ();
+  af->safe_pointer = ptr;
 }
 
 /* Arrange to call CALLBACK asynchronously after each GC.  The callback
@@ -388,13 +393,20 @@ async_gc_finalizer (void *ptr, void *data)
    expand the heap.  Therefore scm_i_register_async_gc_callback is
    inappropriate for using on unbounded numbers of callbacks.  */
 void
-scm_i_register_async_gc_callback (void (*callback) (void))
+scm_i_register_async_gc_callback (void (*callback) (void),
+                                  struct async_finalizer *af)
 {
-  void **obj = GC_MALLOC_ATOMIC (sizeof (void*));
+  void **a = GC_MALLOC_ATOMIC(sizeof (void*));
+  void **b = GC_MALLOC_ATOMIC(sizeof (void*));
 
-  obj[0] = (void*)callback;
+  af->safe_pointer = a;
+  af->callback = callback;
 
-  scm_i_set_finalizer (obj, async_gc_finalizer, NULL);
+  *a = af;
+  *b = af;
+
+  scm_i_set_finalizer (a, async_gc_finalizer, NULL);
+  scm_i_set_finalizer (b, async_gc_finalizer, NULL);
 }
 
 
diff --git a/libguile/finalizers.h b/libguile/finalizers.h
index a92a74be1..e3c300aae 100644
--- a/libguile/finalizers.h
+++ b/libguile/finalizers.h
@@ -28,6 +28,11 @@
 
 typedef void (*scm_t_finalizer_proc) (void *obj, void *data);
 
+struct async_finalizer {
+  void *safe_pointer;
+  void (*callback)(void);
+};
+
 SCM_INTERNAL void scm_i_set_finalizer (void *obj, scm_t_finalizer_proc,
                                        void *data);
 SCM_INTERNAL void scm_i_add_finalizer (void *obj, scm_t_finalizer_proc,
@@ -40,7 +45,8 @@ SCM_INTERNAL void scm_i_finalizer_pre_fork (void);
 /* CALLBACK will be called after each garbage collection.  It will be
    called from a finalizer, which may be from an async or from another
    thread. */
-SCM_INTERNAL void scm_i_register_async_gc_callback (void (*callback) (void));
+SCM_INTERNAL void scm_i_register_async_gc_callback (void (*callback) (void),
+                                                    struct async_finalizer 
*af);
 
 /* Return true if THREAD is the finalizer thread.  */
 SCM_INTERNAL int scm_i_is_finalizer_thread (struct scm_thread *thread);
diff --git a/libguile/weak-set.c b/libguile/weak-set.c
index 93c429346..e81b5fdf6 100644
--- a/libguile/weak-set.c
+++ b/libguile/weak-set.c
@@ -902,6 +902,6 @@ void
 scm_init_weak_set ()
 {
 #include "weak-set.x"
-
-  scm_i_register_async_gc_callback (vacuum_all_weak_sets);
+  static struct async_finalizer af;
+  scm_i_register_async_gc_callback (vacuum_all_weak_sets, &af);
 }
diff --git a/libguile/weak-table.c b/libguile/weak-table.c
index 1e4d8d302..81752a67f 100644
--- a/libguile/weak-table.c
+++ b/libguile/weak-table.c
@@ -841,5 +841,5 @@ scm_init_weak_table ()
 {
 #include "weak-table.x"
 
-  scm_i_register_async_gc_callback (vacuum_all_weak_tables);
+  //  scm_i_register_async_gc_callback (vacuum_all_weak_tables);
 }
--8<---------------cut here---------------end--------------->8---

Thanks,
Olivier
-- 
Olivier Dion



Reply via email to