# C struct garbage collection not run frequently enough

**URL:** https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919
**Category:** General Usage
**Tags:** garbage-collection, mutable-structure, c, gc
**Created:** [July 11, 2024, 3:38pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919 "2024-07-11T15:38:28Z")
**Posts on this page:** 20
**Page:** 1

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 11, 2024, 3:38pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/1 "2024-07-11T15:38:28Z")

</div>

I have a Julia interface to a C library where each C struct represents a number. In Julia, the mutable struct just contains a `Ptr` to the C struct, and the finalizer calls the C destructor.

These C structs can be very memory intensive (they represent Taylor series), but I’ve noticed Julia’s GC does not seem aware of how much memory is actually being used by the C code.

For example, if I call the function:

```julia
function foo(x)
  for i = 1:10000
    # operations using the C number type
  end
end

```

while keeping an eye on `top` command on my Mac command line, Julia’s memory usage can skyrocket to 5+ GB, and then if I call it again go up to 10+GB, etc until 30+ GB (I’m on a Mac M2 Ultra).

If I instead call the function

```julia
function foo(x)
  for i = 1:10000
    # operations using the C number type
    GC.gc(false)
  end
end

```

there is no skyrocketing of the memory, and in fact, the speed is basically unchanged.

So my questions are

1. Why is this happening? Is Julia’s GC not aware of how much memory is actually being used by the C code?
2. If it is unaware, then how can I tell the GC at object creation in a performant way how much memory is actually being used? If it is aware, then how can I fix this?

---

<div class="post-metadata">

### Author: ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)
#### Post date: [July 11, 2024, 3:47pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/2 "2024-07-11T15:47:25Z")

</div>

Julia is not aware how much memory is behind any given pointer (because it’s just an opaque pointer probably allocated by some other memory allocator). But it should be aware of the overall system memory pressure (including the things allocated by your C library) and run GC when that is tight.

Are you actually getting out of memory errors?

---

<div class="post-metadata">

### Author: ![mikmoore](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mikmoore/32/31109_2.png) [@mikmoore](https://discourse.julialang.org/u/mikmoore)
#### Post date: [July 11, 2024, 3:58pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/3 "2024-07-11T15:58:25Z")

</div>

I believe the Julia GC can only run when an allocation occurs (or when called via `GC.gc`). If you aren’t making _any_ Julia allocations in your code then there may not be an opportunity for the GC to trigger.

---

<div class="post-metadata">

### Author: ![fatteneder](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/fatteneder/32/33991_2.png) [@fatteneder](https://discourse.julialang.org/u/fatteneder)
#### Post date: [July 11, 2024, 4:15pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/4 "2024-07-11T16:15:18Z")

</div>

> [@mattsignorelli](#):
>
> 1. If it is unaware, then how can I tell the GC at object creation in a performant way how much memory is actually being used? If it is aware, then how can I fix this?

You have to allocate the memory in Julia and pass a pointer to it to C.  
E.g.

```julia

mutable struct TheCtype
   x::Cint
end

function do_something(ct::TheCtype)
   p = pointer_from_objref(ct)
   GC.@preserve ct begin
      @ccall my_c_function(p::Ptr{Cvoid})::Cvoid
   end
end

```

---

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 11, 2024, 4:40pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/5 "2024-07-11T16:40:33Z")

</div>

> [@cjdoris](#):
>
> Are you actually getting out of memory errors?

On my smaller laptop, the OS itself will actually kill Julia unless I include `GC.gc(false)`.

On the Mac, things will get extremely slow until eventually around 115 GB memory usage the garbage collector is run, which then takes a significant amount of time too. Shouldn’t it be run long before things get so slow?

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 11, 2024, 4:54pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/6 "2024-07-11T16:54:48Z")

</div>

There’s a few things here that make it hard to help - what version of julia are you using? Is the high memory usage you’re seeing from just the julia process, or overall utilization? How much of this is actually in use vs. just reserved memory (check RSS memory utilization)?

Ideally, could you provide a small reproducible example that we could run on our machines?

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 11, 2024, 4:58pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/7 "2024-07-11T16:58:59Z")

</div>

Also, you mention the use of `finalizer` to free the object. This is not generally a good idea - finalizers are not guaranteed to run immediately, so they may cause your objects to stay alive for much longer than necessary.

If what you’re doing is trying to emulate RAII - there are [better ways](https://seelengrab.github.io/articles/RAII%20in%20Julia/) to do that.

---

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 11, 2024, 5:15pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/8 "2024-07-11T17:15:24Z")

</div>

> [@Sukera](#):
>
> what version of julia are you using?

I’m on 1.10.3

> [@Sukera](#):
>
> Is the high memory usage you’re seeing from just the julia process, or overall utilization? How much of this is actually in use vs. just reserved memory (check RSS memory utilization)?

The 115 GB is what is shown under MEM from `top` for only the `julia` process. I’m not exactly sure how to check if its RSS or not but I’ll figure that out

> [@Sukera](#):
>
> Ideally, could you provide a small reproducible example that we could run on our machines?

Absolutely! Here you go

```julia
import Pkg
Pkg.add("GTPSA")
using GTPSA

d1 = Descriptor(6,10)
r = vars()
iter = 20000

function test!(r, iter)
  for i=1:iter
    normL = 1/sqrt( (1+r[6])^2- r[2]^2 - r[4]^2)
    r[5] = r[5] + normL * (1 + r[6]) - 1
    r[1] = r[1] + normL * r[2]
    r[3] = r[3] + normL * r[4]
  end
end

function testgc!(r, iter)
  for i=1:iter
    normL = 1/sqrt( (1+r[6])^2- r[2]^2 - r[4]^2)
    r[5] = r[5] + normL * (1 + r[6]) - 1
    r[1] = r[1] + normL * r[2]
    r[3] = r[3] + normL * r[4]
    GC.gc(false)
  end
end

test!(r, iter)
testgc!(r, iter)

```

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 11, 2024, 5:28pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/9 "2024-07-11T17:28:21Z")

</div>

> [@mattsignorelli](#):
>
> Absolutely! Here you go

Thank you! The allocations are due to your `TPS` object having to live on the heap due to your `finalizer`:

```julia-auto
julia> using AllocCheck

julia> try 
           test!(r, iter)
       catch e
           e.errors[1]
       end
Allocation of TPS in /home/sukera/.julia/packages/GTPSA/YxRfI/src/tps.jl:5
  | t = new(t1)

julia> try 
           test!(r, iter)
       catch e
           e.errors[2]
       end
Allocating runtime call to "jl_f_finalizer" in ./gcutils.jl:87
  | Core.finalizer(f, o)

```

The allocation can’t be elided because of the unknown lifetime. Since finalizers are _not_ guaranteed to run as soon as the object is “dead”, they accumulate until the next GC run occurs. If I simply insert a `GC.gc()`:

```julia-auto
julia> function test!(r, iter)
         for i=1:iter
           normL = 1/sqrt( (1+r[6])^2- r[2]^2 - r[4]^2)
           r[5] = r[5] + normL * (1 + r[6]) - 1
           r[1] = r[1] + normL * r[2]
           r[3] = r[3] + normL * r[4]
           GC.gc()
         end
       end

```

The continouos high memory usage disappears. Note that “high memory usage” is not always a bad thing - just having “empty” memory around is not magically going to make code more performant.

---

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 11, 2024, 5:35pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/10 "2024-07-11T17:35:25Z")

</div>

> [@Sukera](#):
>
> The continouos high memory usage disappears. Note that “high memory usage” is not always a bad thing - just having “empty” memory around is not magically going to make code more performant.

Thanks for your checks! Yes, if you put `GC.gc(false)` at the end of the loop then there is no overload of memory usage: on my small laptop the OS does not kill Julia, and on my Mac things do not get really slow after many runs.

My point is, shouldn’t Julia’s GC be able to detect this and run the GC more frequently so that this overload of memory and subsequent either killing by the OS or significant slow down does not occur?

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 11, 2024, 5:42pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/11 "2024-07-11T17:42:52Z")

</div>

> [@mattsignorelli](#):
>
> Yes, if you put `GC.gc(false)` at the end of the loop

Note that `GC.gc()` is distinct from `GC.gc(false)`; the former runs GC manually while the latter disables it entirely (after performing a collection, I believe).

> [@mattsignorelli](#):
>
> My point is, shouldn’t Julia’s GC be able to detect this and run the GC more frequently so that this overload of memory and subsequent either killing by the OS or significant slow down does not occur?

You haven’t mentioned a slowdown so far - if it’s getting slower because of that, that’s of course worth looking into. I suspect that any slowdown that does occur because of this (in your particular case) would be explained by having the OS swap out to disk? That’s something you’ll have to investigate though.

Generally, Julia tries to run GC not that often, because empty memory is (more or less) “free real estate”. Garbage collection runs are relatively slow and getting new memory is relatively quick. In recent versions (I don’t know the exact one OTOH, sorry) you can give `--heap-size-hint` to force GC to occur once a threshold is reached, which might be beneficial in your case.

---

<div class="post-metadata">

### Author: ![vchuravy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/vchuravy/32/8_2.png) [@vchuravy](https://discourse.julialang.org/u/vchuravy)
#### Post date: [July 11, 2024, 5:48pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/12 "2024-07-11T17:48:59Z")

</div>

> while the latter disables it entirely (after performing a collection, I believe).

The Boolean being passed decided between a incremental or full collection:

[https://docs.julialang.org/en/v1/base/base/#Base.GC.gc](https://docs.julialang.org/en/v1/base/base/#Base.GC.gc)

---

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 11, 2024, 5:53pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/13 "2024-07-11T17:53:07Z")

</div>

> [@Sukera](#):
>
> You haven’t mentioned a slowdown so far - if it’s getting slower because of that, that’s of course worth looking into.

I said this here:

> [@mattsignorelli](#):
>
> On my smaller laptop, the OS itself will actually kill Julia unless I include `GC.gc(false)`.
> 
> On the Mac, things will get extremely slow until eventually around 115 GB memory usage the garbage collector is run, which then takes a significant amount of time too. Shouldn’t it be run long before things get so slow?

Yes I would not be concerned if it wasn’t slowing down and/or being killed by the OS

---

<div class="post-metadata">

### Author: ![vchuravy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/vchuravy/32/8_2.png) [@vchuravy](https://discourse.julialang.org/u/vchuravy)
#### Post date: [July 11, 2024, 9:08pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/14 "2024-07-11T21:08:36Z")

</div>

You can use `jl_malloc`/`jl_calloc`/`jl_free` to allocate memory in a way that is visible to the GC. You could set a function pointer in your library to these functions, and then use that function pointer.

---

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 12, 2024, 1:22am UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/15 "2024-07-12T01:22:58Z")

</div>

This is interesting. The library uses its own special allocator instead of malloc by default (uses a thread-safe pool of memory), but I’d like to try `jl_malloc` since it actually might be faster in this case. I haven’t worked with function pointers in C before, could you point me to some documentation or give a simple example of how to change my `malloc` to `jl_malloc`? Do I need to include and link against any Julia library?

---

<div class="post-metadata">

### Author: ![PatrickHaecker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/patrickhaecker/32/222891_2.png) [@PatrickHaecker](https://discourse.julialang.org/u/PatrickHaecker)
#### Post date: [July 12, 2024, 4:57am UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/16 "2024-07-12T04:57:35Z")

</div>

You do not need to link, as `julia` already is linked against it (it’s probably `libjulia`, but I haven’t checked). So you can directly use

```julia
@ccall jl_malloc(4::Csize_t)::Ptr{Cvoid}

```

Obviously, you need to replace the `4` by the needed size.

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 12, 2024, 5:29am UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/17 "2024-07-12T05:29:58Z")

</div>

> [@vchuravy](#):
>
> The Boolean being passed decided between a incremental or full collection:

Ah right, mixed it up with `GC.enable` 🤦

> [@mattsignorelli](#):
>
> This is interesting. The library uses its own special allocator instead of malloc by default (uses a thread-safe pool of memory), but I’d like to try `jl_malloc` since it actually might be faster in this case

If the library doesn’t provide a way to use a custom `malloc`, it’ll be difficult to just drop `jl_malloc` in there.

* * *

One thing I’m wondering about, looking at the [struct definition](https://github.com/bmad-sim/GTPSA.jl/blob/689ee9e0a17ab47dacb99ef6daa922bacf36adcf/src/tps.jl#L2), is why you’re wrapping a pointer? Ordinarily, I’d make the [`RTPSA` struct](https://github.com/bmad-sim/GTPSA.jl/blob/689ee9e0a17ab47dacb99ef6daa922bacf36adcf/src/low_level/rtpsa.jl#L16) mutable (to communicate to julia that it has a stable address) and just use that directly, perhaps attaching the finalizer there, retrieving the object pointer through `pointer_from_objref` (which should be safe here, since the object is alive in the finalizer). Why the additional indirection?

---

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 12, 2024, 2:25pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/18 "2024-07-12T14:25:15Z")

</div>

> [@Sukera](#):
>
> If the library doesn’t provide a way to use a custom `malloc`, it’ll be difficult to just drop `jl_malloc` in there.

There is an option to use standard `malloc`, `calloc`, `free` etc, so I could drop in the `jl_malloc` in there. Then I would just have to include `libjulia` and link against it, right?

> [@Sukera](#):
>
> One thing I’m wondering about, looking at the [struct definition](https://github.com/bmad-sim/GTPSA.jl/blob/689ee9e0a17ab47dacb99ef6daa922bacf36adcf/src/tps.jl#L2), is why you’re wrapping a pointer? Ordinarily, I’d make the [`RTPSA` struct](https://github.com/bmad-sim/GTPSA.jl/blob/689ee9e0a17ab47dacb99ef6daa922bacf36adcf/src/low_level/rtpsa.jl#L16) mutable (to communicate to julia that it has a stable address) and just use that directly, perhaps attaching the finalizer there, retrieving the object pointer through `pointer_from_objref` (which should be safe here, since the object is alive in the finalizer). Why the additional indirection?

The `RTPSA` is initialized entirely in the C code, and the constructors just return pointers to them. C owns all of the memory here. Every operation/function takes in pointers to `RTPSA` in the C library, so doesn’t it make sense to just have the Julia side handle moving the pointers around properly? If I instead use only the `RTPSA` struct, then at each `RTPSA` construction I’d have to do an `unsafe_load` and each operation a `pointer_to_objref` for each. And even still, Julia doesn’t know how long the `coef` array is in `RTPSA`, which is the most memory intensive array in the struct here.

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [July 12, 2024, 2:49pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/19 "2024-07-12T14:49:15Z")

</div>

> [@mattsignorelli](#):
>
> There is an option to use standard `malloc`, `calloc`, `free` etc, so I could drop in the `jl_malloc` in there. Then I would just have to include `libjulia` and link against it, right?

Julia itself already loads `libjulia`, so that should just work if you’re calling the library from julia.

> [@mattsignorelli](#):
>
> Every operation/function takes in pointers to `RTPSA` in the C library, so doesn’t it make sense to just have the Julia side handle moving the pointers around properly?

What I’m saying is that the pointer _is_ the object itself; Julia just abstracts that detail away. Mutable structs in Julia have pointer identity. In that way, Julia already handles the pointer correctly for you and you don’t have to emulate that manually.

> [@mattsignorelli](#):
>
> If I instead use only the `RTPSA` struct, then at each `RTPSA` construction I’d have to do an `unsafe_load` and each operation a `pointer_to_objref` for each.

You don’t have to do that manually though - the `ccall` interface handles all of that for you. See [here](https://docs.julialang.org/en/v1/manual/calling-c-and-fortran-code/#automatic-type-conversion). You’d implement `Base.unsafe_convert` for `RTPSA` to give `pointer_from_objref` and then you can just pass the object as-is back to C.

> [@mattsignorelli](#):
>
> And even still, Julia doesn’t know how long the `coef` array is in `RTPSA`, which is the most memory intensive array in the struct here.

Julia doesn’t need to know that though - your struct would be exactly the same, pointer fields and all. If you want to inspect the array you can implement some accessing sugar to handle the `unsafe_load` for you (or use `unsafe_wrap` with `own=false` for printing), but other than that, you don’t need to change anything about your struct.

The advantage of this approach lies mainly in the fact that there are no allocations at all anymore. The memory was already allocated by C, there’s no wrapper and since every struct is freed in the same way, the function passed to `finalizer` can also just be a regular function instead of an anonymous function.

---

<div class="post-metadata">

### Author: ![mattsignorelli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mattsignorelli/32/221502_2.png) [@mattsignorelli](https://discourse.julialang.org/u/mattsignorelli)
#### Post date: [July 12, 2024, 3:10pm UTC](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919/20 "2024-07-12T15:10:10Z")

</div>

OK I might be convinced… I just have two questions:

1. When I construct an `RTPSA`, the C constructor will give a pointer `Ptr{RTPSA}`. If I understand correctly, with this pointer I then `unsafe_load` into my `RTPSA` struct. Now when the finalizer is eventually called, I pass it `pointer_from_objref` for that `RTPSA`. This pointer is _different_ from the pointer originally returned by the C code during construction. Do they both point to the exact same place in memory, and is the memory properly freed on the C side when receiving this different pointer?
2. I cannot compile the C to a shared library with these undefined symbols `jl_free` and `jl_malloc`. So I’m not exactly sure what you mean by it will “just work”?

[Next page](https://discourse.julialang.org/t/c-struct-garbage-collection-not-run-frequently-enough/116919.md?page=2)
