Back to Articles

Simplex 0.20.0: The Road Less Traveled

One Data Type. Native Machine Code. Faster Than C.

“Two roads diverged in a wood, and I—
I took the one less traveled by,
And that has made all the difference.”
— Robert Frost

How do you make a runtime faster than C on classical architecture? That was the question behind Simplex 0.20.0. The answer turned out to be deceptively simple: stop pretending your data is something it isn't. Simplex has one data type—dual32—a value and its derivative, packed together, flowing through every computation. C doesn't know this. Simplex does. And that single piece of knowledge, carried all the way down to the register level, changes everything.


The Insight: One Type to Rule Them All

Every language has a type system. Integers, floats, strings, structs, enums—layers of abstraction built over decades. Simplex asked a different question: what if there was only one type?

A dual32 is two 16.16 fixed-point numbers packed into 64 bits. The upper 32 bits hold the value. The lower 32 bits hold its derivative. That's it. Every number in Simplex is a dual32. Every string is a Vec<dual32>. Every struct, every collection, every tensor—all dual32, all the way down.

// dual32: two 16.16 fixed-point values in one 64-bit integer
//
//   [  value (32 bits)  |  derivative (32 bits)  ]
//   [  16.16 fixed-pt   |  16.16 fixed-pt        ]
//
// Range: ±32,767 per component
// Precision: ~0.00002 (1/65536)
// Forward-mode AD: native, always, free

typedef int64_t dual32;

This is not a compromise. This is a design decision that propagates upward into everything the language can do. Every arithmetic operation automatically computes derivatives. Forward-mode automatic differentiation isn't a library feature—it's a consequence of the data type. You don't import it. You don't enable it. It's just there, in every + and * and sin() you write.

But the real gain isn't AD for free. The real gain is what this uniformity enables at the bottom of the stack.


Registers: Where the Real Work Happens

Every computation, on every processor ever built, comes down to the same thing: load values into registers, operate on them, store the results. The cost of computation is dominated not by the operations themselves—an add takes one cycle—but by the movement of data in and out of registers. Load/store is the bottleneck. Always has been.

Now consider what happens when C compiles dual number arithmetic. C sees a 64-bit integer. It doesn't know that integer contains two meaningful components. It loads the whole thing, operates on the whole thing, stores the whole thing. If you need the value and derivative separately—and you always do, because dual number multiplication requires cross-terms—you shift and mask. Every operation unpacks, computes, repacks. Shift right 32. Mask with 0xFFFFFFFF. Do the math. Shift left 32. OR them back together. Repeat.

Simplex knows something C doesn't: a dual32 is always two components. So at the machine code level, Simplex keeps them in register pairs. Value in one register, derivative in another. They never get packed together in the hot loop. There is no shift. There is no mask. There is no repack.

// What C does (even at -O2):
//   load packed dual32 into r0
//   shift right 32 to extract value    ← wasted cycle
//   mask to extract derivative          ← wasted cycle
//   compute add on both components
//   shift left 32, OR to repack         ← wasted cycle
//   store packed dual32

// What Simplex native does:
//   r0 = value, r1 = derivative        ← already separated
//   add r0, r2                          ← value + value
//   add r1, r3                          ← deriv + deriv
//   (no pack/unpack, ever)

This is not a compiler optimisation. It's not a clever trick. It's a structural advantage that exists because the language has one data type and the machine code backend knows it.


DualVec: Structure-of-Arrays All the Way Down

The register-pair principle extends into memory. A DualVec—Simplex's only compound data structure—stores values and derivatives in separate contiguous arrays:

// DualVec: Structure-of-Arrays layout
//
//   vals[]:   [v0, v1, v2, v3, ...]   contiguous int32_t
//   derivs[]: [d0, d1, d2, d3, ...]   contiguous int32_t
//
// NOT: [(v0,d0), (v1,d1), (v2,d2), ...]   ← what C would do

struct DualVec {
    vals:   *int32_t,    // contiguous value components
    derivs: *int32_t,    // contiguous derivative components
    len:    usize,
    cap:    usize,
}

This is the same insight as the register pairs, lifted one level up. When you need to operate on all values in a vector—which is the common case in neural network layers, tensor operations, signal processing—the values are contiguous in cache. The derivatives are contiguous in cache. The processor prefetcher works because the access pattern is linear. SIMD works because the components are homogeneous arrays of int32_t.

An Array-of-Structs layout interleaves value and derivative, halving your effective cache utilisation for any component-wise operation. SoA doesn't. This isn't theory. It's cache lines.


Native Machine Code: No LLVM, No C, No Dependencies

Previous versions of Simplex compiled to LLVM IR, which then passed through LLVM's optimiser and code generator. That pipeline is powerful and general-purpose. It's also a black box that doesn't understand dual32.

Simplex 0.20.0 introduces a native machine code backend. The compiler translates Simplex source directly to ARM64 or x86_64 machine instructions, with no intermediate representation, no external toolchain, and no dependencies. The entire path from source to executable is Simplex.

// Machine ops: the instruction set Simplex actually uses
//
// Standard ops:  mov, load, store, add, sub, mul, div, cmp, branch
// Dual32 ops:    d32_add, d32_sub, d32_mul
//
// The dual32 ops emit register-pair instructions directly.
// d32_add becomes two native adds. d32_mul becomes the cross-term
// sequence. No abstraction layer. No function call. Just instructions.

fn MACH_OP_ADD() -> i64 { 5 }      // integer add
fn MACH_OP_SUB() -> i64 { 6 }      // integer sub
fn MACH_OP_MUL() -> i64 { 7 }      // integer mul
fn MCG_D32_ADD() -> i64 { 32 }     // dual32 add (2 register adds)
fn MCG_D32_SUB() -> i64 { 33 }     // dual32 sub (2 register subs)
fn MCG_D32_MUL() -> i64 { 34 }     // dual32 mul (cross-term sequence)

The register allocator knows about register pairs. The instruction selector knows about dual32 semantics. The encoder knows how to emit the exact byte sequence for each target architecture. Every layer of the backend is aware that it's compiling dual32 operations, and every layer exploits that knowledge.

The hot loop for 10 million dual32 add-multiply-subtract operations compiles to 107 bytes of machine code. Zero memory access. Zero function calls. Just register arithmetic in a tight loop.


The Benchmark

Numbers don't lie. We benchmarked 10 million iterations of the fundamental dual32 operation sequence: add, multiply, subtract. Every implementation computes the same result. Every implementation was run on the same machine.

Implementation Time vs Simplex Native
Simplex native (register pairs) 33 ms
Simplex native (full derivatives) 35 ms 1.06x
C separated (register sim) -O2 53 ms 1.6x slower
C packed -O2 54 ms 1.6x slower
C separated (register sim) -O0 78 ms 2.4x slower
C packed -O0 124 ms 3.8x slower
Simplex via LLVM -O2 180 ms 5.5x slower
Simplex via LLVM -O0 666 ms 20x slower

905 million

dual32-ops per second

107-byte hot loop • zero memory access • zero function calls

Read that table carefully. Simplex native at 33ms beats optimised C at -O2 at 53ms. Not unoptimised C. Not interpreted code. C with the full weight of decades of compiler optimisation research behind it, running the same arithmetic on the same hardware.

Why? Because C's optimiser is general-purpose. It sees int64_t values being shifted, masked, and recombined. It can optimise the shift sequences, it can unroll the loop, it can schedule instructions—but it cannot discover that the int64_t is two components that should live in separate registers permanently. That knowledge doesn't exist in C's type system. It can't be inferred. It can only be known.

Simplex knows.


Why Not Just Use Two Variables in C?

Fair question. The “C separated (register sim)” row in the benchmark is exactly that: C code that manually keeps value and derivative in separate variables, mimicking what Simplex does natively. It still loses, at 53ms vs 33ms.

Two reasons. First, C's calling conventions and ABI require packing and unpacking at function boundaries. Simplex's native backend doesn't—register pairs are a first-class concept in the calling convention. Second, C's optimiser makes decisions about register allocation, spilling, and scheduling without knowing that certain register pairs must stay together. Simplex's register allocator knows.

You can hand-write assembly to match Simplex's output. You can't get there through C.


What This Means for Everything Built on Simplex

This isn't just a runtime curiosity. dual32 as the universal type means:

  • Neural networks get forward-mode AD on every operation for free. No tape. No backward pass overhead for gradient estimation.
  • Belief systems propagate uncertainty through derivatives—a belief's confidence is literally its derivative component.
  • Signal processing operates on fixed-point directly, matching DSP hardware semantics without float conversion.
  • Edge deployment runs on devices with no FPU. dual32 is integer arithmetic. It runs on anything with a 64-bit integer unit.
  • Saturation at ±32,767 isn't a crash—it's a signal. The scaffold's hardware boundary detects it and responds, the same way a neural network responds to gradient explosion.

The entire Simplex ecosystem—the standard library, the learning framework, the conduit system, the sieve, the scaffold—all rebuilt on dual32. One type, uniform from register to application.


The Architecture

The 0.20.0 stack, bottom to top:

Layer What Key Property
dual32 Core type: 16.16 fixed-point dual number One type, one format, everywhere
DualVec Structure-of-Arrays memory layout Contiguous components, SIMD-ready
Machine layer Abstract machine instructions + register allocator Register pairs as first-class concept
Codegen AST → MachOps translation Dual32-aware instruction selection
Platform encoders ARM64 and x86_64 binary emission MachOps → raw bytes, no assembler
Runtime Allocator, crypto, TLS 1.3 Zero external dependencies

No LLVM. No libc dependency beyond what the kernel provides. No OpenSSL—TLS 1.3 and X25519 are implemented natively. The compiler bootstraps from Simplex source. The entire toolchain is self-contained.


The Scaffold Watches

One consequence of a single data type: the system can monitor everything. The scaffold—Simplex's self-organising feedback architecture—now has boundaries at every layer: code, model, protocol, hardware, data. When a dual32 saturates at ±32,767, the hardware boundary detects it. When derivatives explode, the model boundary responds. When data flows exceed thresholds, the protocol boundary adapts.

This is possible because there's only one type to watch. A scaffold for a language with 15 numeric types would need 15 monitoring paths. Simplex needs one.


Fixed-Point: A Deliberate Choice

Why 16.16 fixed-point instead of floating-point?

Three reasons. First, integer arithmetic is deterministic—the same operation on any architecture produces the same result. No floating-point rounding surprises. No -ffast-math flag changing your answers. Second, integer operations are uniformly one cycle on every processor, including edge devices that may not have a floating-point unit. Third, fixed-point addition is just integer addition. There's no denormalization, no special-case handling for subnormals, no NaN propagation logic. The hardware does exactly what you'd expect.

The tradeoff is range: ±32,767 with ~0.00002 precision per component. For values that exceed this range, you normalise. For vectors and tensors, the DualVec's SoA layout handles it. The saturation limit isn't a limitation—it's a design constraint that keeps every value in the space where fixed-point arithmetic is exact and fast.


What Changed from 0.18.0

Everything below the surface. The Simplex language syntax hasn't changed. Your .sx files from 0.18.0 still compile. But underneath:

  • The runtime was rebuilt on dual32 and DualVec, replacing the previous mixed-type system
  • The compiler backend gained native machine code emission (ARM64 and x86_64)
  • The standard library was rebuilt—every module now operates on dual32
  • simplex-learning (neural ODEs, KAN, Hopfield, MoE, SDM) rebuilt on dual32
  • simplex-conduit (IO, HTTP, SQL, streams) rebuilt on dual32
  • simplex-sieve (entropy, compression, chunking) rebuilt on dual32
  • OpenSSL removed—replaced with native TLS 1.3 and X25519
  • The scaffold system expanded with six boundaries: code, model, protocol, hardware, data, and contraction engine

179 tests pass. The repo moves to github.com/senuamedia/simplex.


The Road Diverged

Most language runtimes follow the same road: adopt LLVM, use IEEE 754 floats, optimise within the constraints of a general-purpose type system. It works. It's well-understood. It benefits from decades of engineering.

Simplex took the other road. One data type. Native machine code. Register pairs. Structure-of-arrays. Fixed-point arithmetic. Forward-mode AD as a consequence of the representation, not an addition to it.

The result is a language runtime that beats optimised C at the operations that matter—not because the code is faster, more efficient, or “better”—but because Simplex knows what a dual32 is. C doesn't. And when you carry that knowledge all the way from the type system to the register allocator to the instruction encoder, 1.6x falls out naturally.

Simplex native machine code is 1.6x faster than optimised C.
Because C doesn't know dual32 has two components.
We do. We keep them in separate registers.
And we never pack/unpack in the hot loop.

The road diverged. Simplex took the one less traveled by.


Try It

Simplex 0.20.0

Source, documentation, and benchmarks

github.com/senuamedia/simplex →