GC occurs at the worst time in tight loop (Garbage Collection)

julia> @code_llvm x.rfhwil_ccall_interface.julia_set_dB_gain(1.0)
;  @ C:\mypath\src\rfhwil_ccall_interface.jl:375 within `julia_set_dB_gain`
; Function Attrs: uwtable
define void @julia_julia_set_dB_gain_1762(double %0) #0 {
top:
;  @ C:\mypath\src\rfhwil_ccall_interface.jl:376 within `julia_set_dB_gain`
; ┌ @ refvalue.jl:57 within `setindex!`
; │┌ @ Base.jl:38 within `setproperty!`
    store i8 0, i8* inttoptr (i64 140710129657280 to i8*), align 64
; └└
;  @ C:\mypath\src\rfhwil_ccall_interface.jl:377 within `julia_set_dB_gain`
; ┌ @ refvalue.jl:56 within `getindex`
; │┌ @ Base.jl:37 within `getproperty`
    %1 = load atomic {}*, {}** inttoptr (i64 140710129665216 to {}**) unordered, align 64
    %.not = icmp eq {}* %1, null
    br i1 %.not, label %fail, label %pass

fail:                                             ; preds = %top
    call void @ijl_throw({}* inttoptr (i64 140710407392992 to {}*))
    unreachable

pass:                                             ; preds = %top
; └└
; ┌ @ Base.jl:38 within `setproperty!`
   %2 = bitcast {}* %1 to i8*
   %3 = getelementptr inbounds i8, i8* %2, i64 248
   %4 = bitcast i8* %3 to double*
   store double %0, double* %4, align 8
; └
;  @ C:\mypath\src\rfhwil_ccall_interface.jl:378 within `julia_set_dB_gain`
  ret void
}

Base.@ccallable function julia_set_dB_gain(dB_gain::Cdouble)::Cvoid 
    isCalculationValid[]=false  # Parameter Set, so invalidate current answer
    inputRef[].dB_gain = dB_gain
    return nothing
end

f(x) = ccall("extern julia_set_dB_gain", llvmcall, Cvoid, (Cdouble,), x)


using BenchmarkTools

julia> @benchmark f(1.0)
BenchmarkTools.Trial: 10000 samples with 998 evaluations.
 Range (min … max):  16.092 ns … 230.391 ns  ┊ GC (min … max): 0.00% … 83.13%
 Time  (median):     17.305 ns               ┊ GC (median):    0.00%
 Time  (mean ± σ):   18.390 ns ±   6.985 ns  ┊ GC (mean ± σ):  0.48% ±  2.21%

  ▂     ▆█▄▃▁▅                           ▁▃▂▁▁       ▁         ▂
  █▅▁▄▅▆███████▅▅▇▆▃▃▄▁▁▃▃▄▄▅▄▄▅▆█▇▇▆▅▅▄▄██████▇▆▅▄▅███▇▇▆▅▄▁▃ █
  16.1 ns       Histogram: log(frequency) by time      26.1 ns <

 Memory estimate: 16 bytes, allocs estimate: 1.

That surprised me, but it seems the argument passing is indeed the culprit…

1 Like

It turns out @cfunction behaves much better here.

julia> const isCalculationValid = Ref(false)
Base.RefValue{Bool}(false)

julia> const inputRef = Ref(1.0)
Base.RefValue{Float64}(1.0)

julia> Base.@cfunction function julia_set_dB_gain(dB_gain::Cdouble)::Cvoid 
           isCalculationValid[]=false  # Parameter Set, so invalidate current answer
           inputRef[] = dB_gain
           return nothing
       end Cvoid (Cdouble,)
Ptr{Nothing} @0x00007fbbd0a4cff0

julia> f(x) = ccall(Ptr{Cvoid}(0x00007fbbd0a4cff0), Cvoid, (Cdouble,), x)
f (generic function with 1 method)

julia> @benchmark f(1.0)
BenchmarkTools.Trial: 10000 samples with 1000 evaluations.
 Range (min … max):  5.999 ns … 41.309 ns  ┊ GC (min … max): 0.00% … 0.00%
 Time  (median):     6.000 ns              ┊ GC (median):    0.00%
 Time  (mean ± σ):   6.049 ns ±  0.491 ns  ┊ GC (mean ± σ):  0.00% ± 0.00%

  █ ▂                                                      ▂ ▁
  █▇█▅▄▄▄▃▃▁▃▃▃▁▁▄▁▁▃▁▁▃▃▃▁▁▁▄▁▃▄▁▁▃▁▁▃▄▁▄▅▆▁▄▁▁▄▁▄▄▃▁▄▃▁▁▃█ █
  6 ns         Histogram: log(frequency) by time     7.11 ns <

 Memory estimate: 0 bytes, allocs estimate: 0.

So instead of using @ccallable you likely want to lookup the function pointer using cfunction at the beginning of your program and then use those as callable pointers.

HT @jameson

2 Likes

That is really cool how you did that. I was trying to figure out how to do that. We need to just star this as Julia gold.

So how do I make it not heap allocate? Do I need to make a structure for everything and pass by reference?

Is it a bug in Julia?

It’s a sub-optimality, partly since the whole create a shared library from Julia story is quite rough…

I think for now the @cfunction hack should (while annoying) get you to zero allocations.

I did open up an issue `@ccallable` allocates for it's arguments · Issue #51894 · JuliaLang/julia · GitHub

1 Like

How do I get at the cfunction call from my C function since I’m dealing with a compiled package? I was trying to not require a Julia install. Just the shared libraries. Do I need to shadow it in the shared package library somehow?

Are you deleting libjulia-compiler? If not you have everything available.

You would do somthing like:

typedef void(*set_dB_gain)(double);

void main
    // ... init Julia etc

    jl_value_t *jl_ptr = nullptr;
    JL_GC_PUSH1(&jl_ptr);
    jl_ptr = jl_eval_string("Base.@cfunction(julia_set_dB_gain, Cvoid, (Cdouble,))");
    set_dB_gain func = (set_dB_gain) jl_unbox_long(jl_ptr);
    JL_GC_POP();
    

   for ... {
        func(1.0);
    }
1 Like

This is good. I found it in the manual as well.
It seems with this way, I need a full julia install. Or do I just need to scavenge the julia.h file for the few functions I call and put them in my own header?

You don’t need a full Julia install, but you do need libjulia, libjulia-internal, and libjulia-codegen + `julia.h|

I don’t understand why you don’t have that header, but maybe that’s a question for PackageCompiler…?

I started looking at all of the includes inside julia.h and it looks like a lot of additional stuff. My thought was, would it be easier to pass pointers as inputs with the @ccallable interface instead of floats for example? That might be easier than hacking the julia.h include.

I don’t understand why you need to hack the julia.h file?
You should just include it and don’t change it?

If PackageCompiler doesn’t easily provide it, we should open an issue to change it.

To make complete for the example. I got a segmentation fault for the lines above because the jl_ptr always came back null. This was because I neglected to include another jl_eval_string("using myPackage") where myPackage contains the julia_set_dB_gain function.

Also, to debug the segmentation fault, open up a julia session and test all of the jl_eval_string lines for mistakes and typos.