Performance Figures

All performance timings on this page were obtained using a mid-range laptop from 2016 with an Intel Core i7-6500U CPU (2.5 GHz). Rust code was compiled with opt-level = 3, lto = true, codegen-units = 1. The "unsafe-internals" feature was enabled unless noted otherwise.

Specimen Project

GameLisp's specimen project is The Castle on Fire, a 2D sidescroller which is still under development. We can use this project to provide some representative performance figures for a real-world game which uses GameLisp.

The project has around 15,000 non-empty, non-comment lines of GameLisp code, which are loaded into the GameLisp runtime in about 550 milliseconds (or less than 100 milliseconds when pre‑compiled).

The entity system, main loop and parts of the physics system are implemented in GameLisp. Each entity receives a minimum of two method calls per frame. In a scene with 292 (mostly trivial) entities, the total execution time spent running GameLisp code is ~2.0 milliseconds per frame, the memory pressure is 180 kilobytes of small allocations per frame, and the GC time is around 0.10 milliseconds per frame, maintaining a 7 megabyte heap.

When we switch to a very busy scene (30 physics-enabled on-screen enemies with behaviour scripting and rendering, all controlled from GameLisp), the GameLisp execution time climbs to ~4.0 milliseconds, the memory pressure to 350 kilobytes, and the GC time to 0.20 milliseconds. Most of this execution time is spent on the physics simulation - moving the hottest parts of the physics engine to Rust would be an easy optimization, if necessary.

If the heap size seems small, note that the game's total memory usage is a few dozen megabytes. The 7 megabyte heap only contains memory managed directly by GameLisp, not including rdata.

When the game is run on a low-end laptop from 2011 (with a Pentium P6100), all GameLisp code execution (including source-file loading and garbage-collection) experiences a constant-factor slowdown of around 2x to 4x relative to the i7-6500U. The game remains very playable.

Benchmarks

GameLisp uses a bytecode interpreter, so I've benchmarked it against other interpreted languages - specifically, the reference implementations of Lua and Python. Please note that JITted scripting language implementations, like LuaJIT, PyPy, V8, and SpiderMonkey, would be significantly faster. (However, integrating those libraries into a Rust game project would not be straightforward.)

The "GameLisp (SI)" column was compiled with the default feature flags, and the "GameLisp (UI)" column was compiled with the "unsafe-internals" flag enabled.

The primitive_x benchmarks perform a basic operation (like addition or array indexing) in an unrolled loop, while the remaining benchmarks try to imitate actual game code.

BenchmarkLua 5.3GameLisp (UI)Python 3.7.7GameLisp (SI)
primitive-inc432.2ms672.1ms3710.3ms1079.8ms
primitive-arith534.0ms535.1ms2298.7ms828.4ms
primitive-call0258.3ms631.9ms740.2ms1036.1ms
primitive-call3592.3ms994.0ms955.7ms1891.2ms
primitive-array76.1ms204.5ms247.4ms390.5ms
primitive-table85.8ms395.9ms269.6ms679.2ms
primitive-field82.9ms217.6ms333.2ms633.6ms
primitive-method489.1ms1275.1ms838.7ms2093.0ms
rects384.0ms1142.5ms1234.7ms2664.8ms
flood_fill400.1ms632.2ms664.3ms976.9ms
rotation657.4ms1015.3ms1325.6ms1843.9ms

By default, GameLisp's performance is inferior to Python. If you've benchmarked your GameLisp scripts and established that they're a performance bottleneck, switching on the "unsafe-internals" flag will roughly double their performance. With that flag switched on, GameLisp's performance currently hovers somewhere between Lua and Python.

Optimizing GameLisp Code

Don't.

I really mean it. Other than occasionally thinking about the big-O complexity of your algorithms, you shouldn't waste any effort at all trying to make GameLisp run fast. Please don't be tempted.

The primary reason is that, as described in Section 2, GameLisp is very closely integrated with Rust. Here's the punchline to those benchmark figures above:

BenchmarkRustGameLisp (UI)GameLisp (SI)
primitive-inc44.1ms672.1ms1079.8ms
primitive-arith126.7ms535.1ms828.4ms
primitive-call014.1ms631.9ms1036.1ms
primitive-call334.5ms994.0ms1891.2ms
primitive-array12.1ms204.5ms390.5ms
primitive-table259.5ms395.9ms679.2ms
primitive-field10.3ms217.6ms633.6ms
primitive-method10.4ms1275.1ms2093.0ms
rects9.6ms1142.5ms2664.8ms
flood_fill2.7ms632.2ms976.9ms
rotation59.4ms1015.3ms1843.9ms

Idiomatic Rust code will often be more than a hundred times faster than idiomatic GameLisp code. Even hand-optimized GameLisp code is usually dozens of times slower than a naive Rust implementation. Rust is incredibly fast.

As I mentioned at the very beginning, GameLisp is intended to be used for the messy, exploratory, frequently-changing parts of your codebase. Whenever I've been faced with a dilemma between making GameLisp fast and making it convenient, I've almost always chosen convenience.

This is why I've put so much effort into giving GameLisp a really nice Rust API. If some part of your GameLisp codebase has unacceptably poor performance, it's usually an easy task to "rewrite it in Rust", in which case you'll immediately reap huge performance gains.

Here's a quick breakdown of the parts of a game codebase which are likely to be better-suited for either Rust or GameLisp, speaking from experience:

  • For binary file-format parsing, rendering, audio mixing, image processing, vertex processing, spatial data structures, collisions, physics simulation, and particle effects, Rust is the obvious winner.

    • That being said, you can definitely manage those parts of your engine using GameLisp. Your .zip file parser might be implemented in Rust, but your resource manager could be implemented in GameLisp. Your physics simulator might be implemented in Rust, but you could also have a PhysicsRect mixin which hooks an entity into the physics system.

    • GameLisp can also be useful for prototyping. For example, if your game procedurally generates its levels, you could use GameLisp to freely experiment with different algorithms, and then reimplement the final algorithm in Rust.

  • Your main loop might belong in Rust, depending on how complicated it is.

    • If your game uses an entity-component system, the bulk of the ECS should definitely be implemented in Rust. Good performance is more-or-less the entire point of an ECS. Consider implementing a Script component which owns a GameLisp object and invokes callbacks on it - or perhaps even one component for each callback, like UpdateScript and DrawScript.
  • For entity behaviour scripting and cutscene scripting, GameLisp is the obvious choice.