In the same way that using R packages cpp11 or Rcpp allows you to bridge your R code with C++ code that runs faster than R, you can use luajr to bridge your R code with Lua code that runs faster than R.
Often, C/C++ code runs about 5-100 times faster than the equivalent R code. In my experience, luajr code typically presents a similar speedup. The amount of speedup also depends strongly on what you’re doing.
Why use luajr then? Rcpp and cpp11 require a C++ toolchain (e.g. gcc, clang, etc.) and requires long compilation times, whereas luajr doesn’t. This means that luajr is usable when a C++ compiler isn’t available, or when compilation times are prohibitive or an annoyance.
In this vignette, we show a few example benchmarks to show situations where luajr offers a substantial improvement in code execution speed and when it doesn’t.
Example 1: when to stick with R
Lots of R’s built-in functions are already speedy because they rely on compiled C code. For example, consider the following ways you might sum all the elements of a numeric vector:
library(luajr)
x <- rnorm(1e6)
# Method one: built-in sum
s1 <- sum(x)
# Method two: sum in R
sum_R <- function(x) {
s <- 0
for (y in x) {
s <- s + y
}
return (s)
}
s2 <- sum_R(x)
# Method three: sum in Lua
sum_L <- lua_func("function(x)
local s = 0
for i = 1, #x do
s = s + x[i]
end
return s
end")
s3 <- sum_L(x)Each method produces the same answer. Using
bench::mark() on a 2025-era MacBook Pro, the execution time
for each method is as follows:
| Method | Median runtime |
|---|---|
sum(x) |
664 µs |
sum_R(x) |
6340 µs |
sum_L(x) |
546 µs |
R’s built-in sum() function calls an internal R function
written in C, so it is already relatively fast. The “naïve”
sum_R() function, which calculates the sum manually, is
around 10x slower. And the Lua version just barely edges out R’s
built-in sum() function, but it’s worth noting that Lua
arithmetic may not handle NA values correctly for numeric
vectors and certainly won’t handle NA values correctly for
integers (without explicit handling).
Example 2: logistic map
Consider the following:
logistic_map_R = function(x0, burn, iter, A)
{
result_x = numeric(length(A) * iter)
j = 1
for (a in A) {
x = x0
for (i in 1:burn) {
x = a * x * (1 - x)
}
for (i in 1:iter) {
result_x[j] = x
x = a * x * (1 - x)
j = j + 1
}
}
return (list2DF(list(a = rep(A, each = iter), x = result_x)))
}
logistic_map_L = lua_func(
"function(x0, burn, iter, A)
local dflen = #A * iter
local aa = luajr.numeric(dflen, 0)
local xx = luajr.numeric(dflen, 0)
local j = 1
for k, a in pairs(A) do
local x = x0
for i = 1, burn do
x = a * x * (1 - x)
end
for i = 1, iter do
aa[j] = a
xx[j] = x
x = a * x * (1 - x)
j = j + 1
end
end
local result = luajr.dataframe()
result:set('a', aa)
result:set('x', xx)
return result
end", "native, native, native, auto")
# To be compiled using Rcpp::cppFunction()
logistic_map_C =
'DataFrame logistic_map(double x0, unsigned int burn, unsigned int iter, NumericVector A)
{
unsigned int dflen = A.length() * iter;
NumericVector da(dflen, 0);
NumericVector dx(dflen, 0);
unsigned int j = 0;
for (auto a : A)
{
double x = x0;
for (unsigned int i = 0; i < burn; ++i)
x = a * x * (1 - x);
for (unsigned int i = 0; i < iter; ++i, ++j)
{
dx[j] = x;
da[j] = a;
x = a * x * (1 - x);
}
}
return DataFrame::create(Named("a") = da, Named("x") = dx);
}'Here we are comparing three different versions (R, Lua, C++) of running a parameter sweep of the logistic map, a chaotic dynamical system popularized by Bob May in a 1976 Nature article. The output looks like this:
logistic_map = logistic_map_L(0.5, 100, 100, 200:385/100)
plot(logistic_map$a, logistic_map$x, pch = ".")
The times taken by each function on a 2025-era MacBook Pro are as follows:
| Call | Method | Median runtime |
|---|---|---|
logistic_map_R(0.5, 100, 100, 200:385/100)) |
R function | 850 µs |
logistic_map_L(0.5, 100, 100, 200:385/100)) |
Lua function | 80 µs |
logistic_map_C(0.5, 100, 100, 200:385/100)) |
C++ function | 100 µs |
The version written in Lua is around 10 times faster than the version in R, and even somewhat outperforms Rcpp. Note that the relative speed of Lua versus C++ depends on the number of iterations, and whether you use Rcpp or cpp11, which each seem to have marginal advantages in different cases. Nonetheless, the Lua version seems to execute about as quickly as the C++ version, within plus or minus 20%.
The speedup of the Lua function relative to R was much more notable
in an earlier test where the R version first created the data frame and
then performed the iteration, i.e. with the line
result$x[j] = x instead of result_x[j] = x.
The median runtime for that R version was two orders of magnitude slower
than the Lua version; the extra overhead associated with
data.frame methods was pointed out by Tim Taylor. This goes
to show that when aiming for efficient R code, it helps to know a few
tricks.
Example 3: Lorenz attractor
Even greater speedups can be seen when conducting more intensive calculations. For example, solving a system of ordinary differential equations (ODEs) is a common scientific task. Normally this is done in R using a general-purpose package such as deSolve. In this benchmark we will instead do simple Euler integration of the famous chaotic Lorenz system.
To calculate this system entirely in R, you might write something like the following:
# Lorenz attractor in R
# Take a single step, size dt, of the Lorenz system
step1 = function(x, rho, sigma, beta, dt)
{
dx = sigma * (x[2] - x[1])
dy = x[1] * (rho - x[3]) - x[2]
dz = x[1] * x[2] - beta * x[3]
x[1] = x[1] + dx * dt
x[2] = x[2] + dy * dt
x[3] = x[3] + dz * dt
return (x)
}
# Calculate Lorenz system trajectory
lorenz1 = function(n, init, rho, sigma, beta)
{
x = init
result = matrix(0, nrow = n, ncol = 3)
for (i in 1:n) {
result[i, ] = x
x = step1(x, rho, sigma, beta, 0.01)
}
return (result)
}
# Generate and plot result
result1 = lorenz1(5000, c(1,1,1), 28, 10, 8/3)
plot(result1[,1], result1[,2], type = "l")
Similar code can be written entirely in Lua using luajr:
# Lorenz attractor in Lua
library(luajr)
lorenz2 = lua_func("
function(n, init, rho, sigma, beta)
local step2 = function(x, rho, sigma, beta, dt)
local dx = sigma * (x[2] - x[1])
local dy = x[1] * (rho - x[3]) - x[2]
local dz = x[1] * x[2] - beta * x[3]
x[1] = x[1] + dx * dt
x[2] = x[2] + dy * dt
x[3] = x[3] + dz * dt
return x
end
local x = init
local result = luajr.matrix(n, 3)
for i = 1,n do
result[i] = x[1]
result[i + n] = x[2]
result[i + 2*n] = x[3]
x = step2(x, rho, sigma, beta, 0.01)
end
return result
end", "native auto native")
result2 = lorenz2(5000, c(1,1,1), 28, 10, 8/3)
plot(result2[,1], result2[,2], type = "l")
Execution times are as follows:
| Call | Method | Median runtime |
|---|---|---|
lorenz1(5000, c(1, 1, 1), 28, 10, 8/3) |
R function | 3300 µs |
lorenz2(5000, c(1, 1, 1), 28, 10, 8/3) |
Lua function | 38 µs |
The Lua code is 85 times faster than the equivalent R code.
As mentioned before, a library such as deSolve is normally used to integrate systems of ODEs and both versions above do execute rather quickly anyway – it may not be worth the trouble here to save 3 milliseconds! But for intensive and complex simulations, it may help to get a substantial speed boost over what base R can do.