Performance Innovations

How Luwu makes classes fast, and when each optimization applies.

Inline fields

An object stores its fields inline, in an order its class fixes up front, instead of in a hash table that grows as keys are added. That makes objects smaller too: an object with two number fields takes 88 bytes, where a table holding the same fields takes 128.

Constant-offset field access

Because the order is fixed, the compiler turns a field access into a constant offset. Inside a method, self.health compiles to a GETOBJECTMEMBER instruction (SETOBJECTMEMBER for writes) that goes straight to the field's slot, with no key lookup and no cache to check. With native code generation, a field read takes about 0.9 nanoseconds against 2.0 for a table, and a write about 0.3 against 4.5. Reading the same field twice in a row costs one read, since constant propagation reuses the first.

Proving what self is

The compiler only takes these shortcuts when it can prove what self is. Every method starts with a self check that raises an error when it's called on the wrong kind of value. Past that check, self is proven to be an object of the method's class, as long as the method never reassigns it. Type annotations deliberately don't count as proof because a wrong one would read the wrong memory instead of failing with an error.

Method inlining

At optimization level 2 (--!optimize 2), Luwu also inlines method calls. The compiler copies the body of a small method into its caller and removes the call entirely. This is where most of the speed of classes comes from. Inlining follows the same rule of proof: a call inlines when the compiler can prove the receiver's class. That covers self:method() inside a class, an object the same code just constructed, and a value checked with class.isinstance in an if or an assert. An inlined body still runs its self check, and still fails loudly on the wrong kind of object. Hot code runs fastest in the same module as the classes it uses because methods only inline within a module.

Trusting type annotations

Proving every receiver gets tedious once objects hold other objects. The --!trust directive lets a file trust its type annotations instead: a parameter, local or field annotated with a class is then enough to pick the method to inline. Trust never lets wrong code run quietly because the inlined self check turns a lying annotation into a runtime error. --!trust is still experimental, and isn't turned on by default yet.

Construction and type tests

Building and testing objects is cheap too. Constructing an object compiles to a single NEWOBJECT instruction with no temporary table and no function call. class.isinstance compiles to one fused test-and-branch instruction that native code drops entirely when the class is already known.