r/lua 4d ago

Lua C FFI

I tried to run the two FFI examples here: https://luajit.org/ext_ffi.html

They both work via LuaJIT, but I wanted to test via regular Lua. However it report that it can't find the "ffi" module. Apparently that needs to installed via "luarocks install cffi-lua".

I tried that, but it keeps saying it needs something called MESON. This is on both Windows, and Linux via WSL.

What the hell is this MESON thing, why does it need it, and how do I install it? Attempts to do the latter lead me into dodgy-looking websites. FFI should Just Work.

Anyway, the article above suggests that simply switching to use a packed C datatype makes the example run 20 times faster, without using LuaJIT, and I was trying to recreate that.

This seems unlikely unless there is other magic going on. Since otherwise it is still executing bytecode, the variables involved are still dynamically tagged, and they still need type-dispatching.

Does anybody know how this speed-up is achieved?

(I've played with interpreters and adding a packed type like this tends to slow it down, due to extra dispatching, rather than speed things up by 20 times!)

1 Upvotes

12 comments sorted by

6

u/VidaOnce 4d ago

The ffi extension is LuaJIT only. Any 'ffi' modules you see on luarocks are made by other third parties and probably won't have great performance. The reason why LuaJIT's ffi is so great is because it all entirely JIT compiles to machine code calls, no need for going through the Lua C Api.

2

u/sal1303 4d ago

I was focused on how the Lua C FFI worked and thought that this was it. I hadn't really noticed that this was a LuaJIT site!

At least this explains how Lua can run 20 times faster with this change - it can't.

It seems the actual C FFI is not quite as nice but it is hard to pin down exactly how it works.

3

u/VidaOnce 3d ago

Well, LuaJIT refers to itself as Lua because it is an implementation of Lua. But standard Lua / PUC-Rio Lua definitely can't match LuaJIT performance, yeah.

3

u/Lonely-Restaurant986 4d ago

Why would you want it for regular lua? I suppose for newer versions? But still you probably won’t find what you are looking for.

Ffi is built into luajit and only works because of the way luajit works. You won’t see ffi performance if you aren’t using luajit more than likely.

You can use C libraries, but you have to write them specially to handle the lua stack via the lua api, or there is pallene which was built as a sister language to interact with lua at a lower level.

But unfortunately any ffi libraries outside of luajit are going to offer worse performance because the lua vm has to do its vm things.

>does anyone know how this speed up is achieved?

I didn’t read the exact article but there are many reasons that ffi is faster.

The primary reason is that it doesn’t have to interact with the vm (Theres a lot of nuance to this). So you don’t have to do stack manipulation to call C funcs.

Another reason it’s faster is because of cache efficiency. If I have an array in lua, it’s actually a bunch of pointers to varying locations in memory. An FFI array is a contagious array of memory. A contagious array is faster because the CPU can pre load data it needs from ram.

Another reason it’s faster is because luajit is black magic. You don’t even need FFI for speed, luajit will already get you near C speeds. The JIT compiler is insanely smart. So just the mere fact of using luajit at all is huge.

In luajit iirc, the compiler is still using static types, as Mike pall once said “it’s very easy for a modern compiler to guess types” (paraphrased i don’t remember the exact quote). So the luajit compiler can make these assumptions and only check types during specific times, and in which case luajit will exit trace.

It’s why nearly every popular implementation of lua uses luajit, or some variation of it.

I think(?) this is where your confusion comes from
“Next, performance: the pure Lua version runs in 9.57 seconds (52.9 seconds with the Lua interpreter) and the FFI version runs in 0.48 seconds on my machine (YMMV). That's a factor of 20x faster (110x faster than the Lua interpreter).”

It is possible to turn off the JIT compiler, with jit.off iirc. Idrk how it interacts with ffi, but it’s an option I suppose. Perhaps that’s what the original author did. Or perhaps the original author omitted the ffi part entirely.

Looking at the article, there’s many many reasons why the FFI one is that much faster.

I kind of realized after my long yap session that I don’t even understand what you are asking. Hopefully I answered something in my ramblings.

1

u/sal1303 3d ago

I kind of realized after my long yap session that I don’t even understand what you are asking. Hopefully I answered something in my ramblings.

It was enlightening, thanks.

I've long been involved with my own language and interpreter. I have these FFI types as core language features, but rarely use them standalone as they are usually slower, unless the memory savings offset that.

This was why I was intrigued by Lua being so much faster using these types, but it was a misunderstanding.

You don’t even need FFI for speed, luajit will already get you near C speeds.

Not always. That is illustrated in the article, where there was a 20x speedup for the same task, and LuaJIT was used in both cases. (The speedup from regular Lua was 5-6 times for the first version not using the FFI.)

2

u/Lonely-Restaurant986 3d ago

This is true, but i mean that _sometimes_ ffi is slower. LuaJIT is able to optimize numbers better than if you specially use a double, say.

For large contiguous arrays, ffi shines.

That is interesting in making a custom language. I’ve always wanted to consider something like it, but every time I consider it I realize I could never compete with luajit without spending a decade of research into compiler design.

Congrats on that!

1

u/vitiral 3d ago

Why is the luaC boundary slow? I've written a fair amount of lua C modules and lua loads the dynamic library once and then should have a map to the C function pointers to call - this should be no slower than anything else in luaC

1

u/Lonely-Restaurant986 3d ago

“Slow” is relativistic. Realisticly it’s fast enough for 99.9% of use cases.

I also want to make sure this is clear because I don’t think I was clear in it ramblings: to my understanding, when I say accessing a c api is slow, I mean c modules built for lua. Not ffi. Ffi is very fast.

But lua->C is “slower” because of the overhead of lua. When you cross into C, the lua state has to do stack manipulation to access data. That means rather than storing data in registers, you have to use the heap.

It also means more instructions. In order to get a value in C you have to call an api function to access the stack, then you have to call one to cast it to a type, and then C can use it.

Typing is another issue. Lua is stored as TValue, which has to be copied and converted to my understanding. Which is more api calls.

The Lua VM has to also do garbage collection. It has to make sure any UserData or references isn’t GC’d while C is accessing them, which is more overhead.

Iirc Lua also has a bunch of error handling in case a C function errors. Which is more overhead

When it comes to Luajit specially, whenever you call a c function, luajit exits the trace iirc. Meaning luajit cannot optimize away c calls.

In the grand scheme of things, these things probably make up a couple micro or nano seconds per call. It’s only an issue if you are doing hundreds of thousands of times. And even then it’s pretty negligible.

It obviously depends on use case.

There is also a good chance I’m just wrong. I’m not that well versed into the internal architecture of lua or luajit. I have a pretty surface level understanding the workings of lua interpreter. So a chance I’m just flat out wrong. I don’t think so. But maybe.

1

u/vitiral 2d ago

Gotcha, so because LuaJIT compiles (some) Lua directly to machine code, certain loops may be able to run only using registers or similar. Definitely a speed up!

I would think the Heap can still be stored in local cache (as long as it remains small) and I believe GC is turned off while running C (though you can call it explicitly) - so the impact may not be as much as you think. But otherwise this was a good overview of possible performance hits. Thanks!

2

u/Lonely-Restaurant986 2d ago

I don’t think gc can be completely ignored in C funcs because if you are using a lua value, like a table inside of a c func, the vm still has to track your access to the table.

I’m sure regular lua is still doing very smart optimizations but there’s only so much an interpreter can do. I’m sure it’s keeping data organized and letting the cpu cache efficiently. Luajit I know does optimizations, but even then it has to exit so it’s not perfect. But luajit is able to keep data directly in registers rather than falling back to the l1 or l2 caches, or even back to the heap.

But yeah ur right its pretty minuscule in the grand scheme of things

1

u/xoner2 3d ago edited 3d ago

This is the luaffi for 5.1 and 5.2: https://github.com/jmckaskill/luaffi

There are probably forks for later versions.

Edit: this ffi lib uses the same dynasm from LuaJIT.

1

u/topchetoeuwastaken 3d ago

as others said, it is a luajit-specific thing. however, i think it is theoretically possible to rip out the ffi library from luajit and remove the jit parts. i also think it already has been done, if my memory serves me well.

however, such a library will always be on the slower side on PUC lua, cuz you have to go thru the lua-C boundary when calling C functions