Skip to main content

Performance

Source: vignettes/articles/performance.qmd

jlbind calls Julia code that was compiled ahead of time, through one .Call() per function. The alternatives, JuliaCall and JuliaConnectoR, run Julia itself: JuliaCall embeds a Julia session in the R process, and JuliaConnectoR starts one in a separate process and talks to it over a socket. This article compares the three on the same Julia functions.

In short, on this machine, jlbind is ready to use 84 to 109 times sooner, and a call costs 0.702 µs rather than 8.76 µs (JuliaCall) or 207 µs (JuliaConnectoR).

The benchmarks

The functions are those of benchjl, a minimal jlbind client kept next to the benchmark script:

greet(name::String) = "Hello, " * name * "!"
shift(x::Vector{Float64}, by::Float64) = x .+ by

jlbind calls them through benchjl, compiled into a jlbind library. JuliaCall and JuliaConnectoR evaluate the same definitions and call them with julia_call() and juliaFun(). Each interface runs in fresh R processes, since jlbind and JuliaCall can’t share one.

  • Startup: the time from loading the package to the result of the first call, greet("world"), in a new R process (R’s own startup excluded), repeated 10 times.
  • Call overhead: the median time of greet("world"), a string in and a string out, after the first call.
  • Data transfer: the median time of shift(x, 1) for numeric vectors x of length 10 to 107, which copies x to Julia and the result back.

The benchmark script is vignettes/articles/performance/benchmark.R. The results below were run on1:

R 4.6.1
Julia 1.13.2
CPU AMD Ryzen 7 8845HS w/ Radeon 780M Graphics
jlbind 0.0.0.9000
JuliaCall 0.17.6
JuliaConnectoR 1.1.6
JuliaCall commit 947d1f3

Startup

Interface Median Min Max
jlbind 36.2 ms 32.6 ms 40.1 ms
JuliaCall 3.94 s 3.88 s 4.28 s
JuliaConnectoR 3.02 s 2.99 s 3.16 s

For jlbind, startup is loading the compiled library and binding the package’s routines. JuliaCall and JuliaConnectoR start Julia, and compile each function the first time it is called. With a real Julia package instead of two one-line functions, compiling and loading the package typically take much longer than here.

Call overhead

Interface Time per call Relative to jlbind
jlbind 0.702 µs 1×
JuliaCall 8.76 µs 12×
JuliaConnectoR 207 µs 295×

A jlbind call is a .Call() into the compiled library, where the arguments are converted with R’s C API and the function is called directly. JuliaCall converts arguments and looks up the function at run time in the embedded session. JuliaConnectoR serializes the call to a separate Julia process and the result back, so each call costs a round trip.

Data transfer

Median time of shift(x, 1) against the length of x, on log scales, for jlbind, JuliaCall and JuliaConnectoR. jlbind is fastest at every length.

Length jlbind JuliaCall JuliaConnectoR
10 1.14 µs 11.1 µs 237 µs
100 1.33 µs 14.5 µs 261 µs
1,000 11.6 µs 43.6 µs 262 µs
10,000 72.8 µs 292 µs 389 µs
100,000 175 µs 2.95 ms 2.16 ms
1,000,000 4.88 ms 33.8 ms 18.9 ms
10,000,000 118 ms 471 ms 291 ms

Footnotes

  1. Using the development version of JuliaCall, which fixes several issues, including failing on Ubuntu 24 (JuliaCall#265).↩︎