Technology10 min read

How close can pure functional programming get to C?

By · Published by Everything Blog

In short

In a recent article, the author shares an impressive result: a DEFLATE compression library written in Lean 4, a functional programming language and proof assistant, outperforms an equivalent Rust package that has been downloaded over a billion times. From the article: Lean-zip** beats every single compression level of **miniz_oxide**, providing the same compression at higher throughput. - However, the article also notes that while **Lean-zip** managed to surpass **Rust's miniz_oxide**, it was no match for the…

Key points

  • In a recent article, the author shares an impressive result: a DEFLATE compression librar…: In a recent article, the author shares an impressive result: a DEFLATE compression library written in Lean 4, a functional programming language and proof assistant, outperforms an equivalent Rust package that has been downloaded over a bill
  • From the article: - **Lean-zip** beats every single compression level of **miniz_oxide**,…: From the article: - **Lean-zip** beats every single compression level of **miniz_oxide**, providing the same compression at higher throughput.
  • - However, the article also notes that while **Lean-zip** managed to surpass **Rust's min…: - However, the article also notes that while **Lean-zip** managed to surpass **Rust's miniz_oxide**, it was no match for the end boss of DEFLATE performance: **libdeflate**, written in C.

Sources checked by our analysis pipeline: 1

How close can pure FP get to C?

October 22, 2026

In Why Lean is Faster Than Rust, the author shares an impressive result: a DEFLATE compression library written in Lean 4, a functional programming language and proof assistant, outperforming an equivalent Rust package that crates.io says has been downloaded over a billion times. From the article:

lean-zip

beats every single compression levelminiz_oxide

provides, i.e. we're capable of providing the same compression at higher throughput. At its fastest setting we're 15% quicker and the output is 8.8% smaller.

However, the article also notes that while they managed to surpass Rust's miniz_oxide

, they were no match for the end boss of DEFLATE performance: libdeflate, written in C.

lean-zip

is certainly not the best here:libdeflate

unsurprisingly blows it out of the water (unsurprisingly because this is a very carefully tuned implementation using architecture-specific SIMD, that we can't touch in Lean).

Enter Roc, a functional programming language (but not a theorem prover) with an explicit focus on runtime performance.

Roc's tagline is "Fast, Friendly, Functional," and Fast comes first because both running fast and compiling fast have always been central goals. Roc is not a mature language (it's still weeks away from its first numbered release, 0.1.0), and its nascent package ecosystem has had almost no optimization work done on it. Still, between its language design, longstanding optimizations, and brand-new compiler implementation, I figured Roc already had enough of a foundation in place to take on the champ: libdeflate.

So I spent a bunch of time grinding through performance optimizations on roc-deflate, a package Niclas Ahden wrote (and uses in paid production work) for DEFLATE compression. I didn't expect to beat libdeflate's performance across the board, but I wanted to see how close it could get on the non-streaming APIs (as roc-deflate hasn't implemented streaming yet), using the classic Silesia corpus for benchmarking and verifying that it produced identical outputs.

Under 10% Slower

Here's where the benchmarks stand today, running on the 2021 M1 MacBook Pro where I did all the optimization work, as well as on an AMD Ryzen 9 3900X Ubuntu machine for an x64 comparison.

roc-deflate ran these 144 benchmarks slower on average than libdeflate, in the median case 8.9% slower on the Ryzen 3900X, and 2.6% on the M1 MacBook Pro.

So while we didn't outperform the champ on average, a median slowdown percentage in the single digits compared to such a carefully-tuned SIMD-powered C library is wild. It's frankly not a result I knew was achievable going in, since Roc is a functional language with automatic memory management racing against code that doesn't have to do bounds checks. (Bounds checks have a cost, but then again, the last time a skipped bounds check in C compression code led to a major Remote Code Execution vulnerability was yesterday.)

The graph shows more detail than just the median gap. On the worst-case inputs, roc-deflate was 18% slower than libdeflate, but on the best-case inputs, roc-deflate was slightly over 18% faster! That's not because Roc has some magical optimization that C can't access; it's just that the two implementations are different, and although on average roc-deflate is slower, for certain inputs it happens to be faster. So while it's possible to get a speedup by switching from libdeflate to roc-deflate, you shouldn't expect it on average.

To be clear, C's performance ceiling is higher than Roc's. For any input where roc-deflate currently outperforms libdeflate, C code can close that gap. The reverse is not true; Roc code is memory-safe, does automatic reference counting, checks bounds, and supports no Rust-style unsafe

to opt out of any of that. These properties give Roc code stronger safety guarantees than code written in C, Zig, Rust, and other languages that allow full access to the hardware—and the price it pays for those guarantees is a lower performance ceiling.

Obviously we can't extrapolate from this one result about how close Roc can get to C in the general case, but now something has been demonstrated that nobody knew was achievable before. As far as I know, this is the most optimization effort that has been put into any Roc package to date, so how close others can get to this result is a complete unknown right now. So while I'm thrilled by this result, I'm fully aware that we won't know how well it generalizes until we've optimized a wider sample of real-world Roc code bases!

What the Performance Gap Buys

An obvious question is "Why settle for any amount slower?" If you could get C levels of performance, why not use libdeflate itself and close that remaining gap?

A pure-Roc implementation has several advantages over a pure-C one:

- Memory safety. Roc code is fully memory-safe and has no language primitives that let you opt out of it. This code is only able to approach C levels of performance because Roc's optimization pipeline determined that many bounds checks and reference counting operations were provably safe to optimize away. Compression packages written in pure Roc don't have access to the unchecked memory operation that caused yesterday's Brotli Remote Code Execution vulnerability.

- No side effects. Roc's type system infers which functions are pure and which can perform side effects. Pure functions' types use a thin arrow, like

Bool -> Bool

, whereas effectful functions use a thick arrow, likeBool => Bool

. (As with memory safety, Roc enforces this and supports no way to opt out.) So when you see that roc-deflate's functions have only thin arrows in their annotations, you know they really are pure functions. - Supply-Chain Security. Roc does not have an arbitrary FFI, meaning you know for sure that roc-deflate's pure functions can't even attempt to do I/O or access arbitrary memory. Given the explosion in supply chain attacks, it's valuable to know that the damage a malicious actor can do with a pure function in a compromised Roc package is limited to returning the wrong value, crashing, or wasting system resources. Roc packages also don't support build scripts, which was the vector used to successfully compromise the popular Rust

arrayref

package two months ago. - Portability. Roc pure functions are designed to be extremely deterministic: they always return the same values when given the same arguments—not just when run on the same machine, but also on any target you compile that code to. (The functions may also crash or fail to terminate, but that behavior should also be deterministic across targets.) The only way a Roc pure function should be expected to behave differently when run on two different systems is if one system runs out of memory mid-execution.

If any of these guarantees don't hold in practice, it can only be due to a defect in Roc's compiler, the platform the application is running on, or the underlying hardware. So when you see in roc-deflate's docs that the entire package is 100% pure functions, you can be confident you are getting all of the above properties from everything the package has to offer. The type system enforces it!

The reason we've been working so hard on all this might become clearer if you look at the proposition in reverse: would you give up guaranteed memory safety, guaranteed lack of side effects, guaranteed deterministic behavior across all targets, and the security benefits of having no exploitable build scripts in packages...for an average speedup percentage in the single digits?

That answer can legitimately vary by use case—Roc's own compiler, for example, wants the maximum performance the hardware can deliver—but I bet a lot of people reading this can see why we're so excited to have achieved this result.

Roc-Specific Optimizations

Most of the optimizations that improved roc-deflate's numbers came from general optimization techniques that are widely beneficial across languages. (I always recommend Casey Muratori's Performance-Aware Programming Series, which I paid for and learned a lot from.) Some of the optimization techniques were Roc-specific, though, and I wanted to mention them here as a reference for others. Even if you're not writing Roc code, this can also give you an idea of how Roc achieves its performance.

Avoid Unnecessary Sharing

One of Roc's most effective optimizations is opportunistic mutation (which Koka calls "Functional but In-Place"). You can learn how it works in Roc's docs on opportunistic mutation, but from a performance optimization perspective, it's critically important to avoid unnecessary sharing so that values remain unique and therefore eligible for in-place mutation.

Hoist Bounds Checks

You can't opt out of bounds checks (e.g. getting a value out of a dynamically-sized collection) in Roc like you can in C, Zig, and Rust, but Roc's compiler can optimize them away if it knows they'd be guaranteed to pass. Bounds checks are most costly inside loops, so you can get a lot of mileage out of adding something like if collection.len() <= highest_index_used { return 0 }

before the loop starts. This adds an additional check that looks redundant, but it can allow the optimizer to safely delete costlier checks inside the loop, once it can tell that failures would not be reachable after that early return.

Build Lookup Tables at Compile Time

Roc's compile-time evaluation can be used to assemble lookup tables at compile time as long as nothing depends on runtime inputs. Even better, since their lengths are known at compile time, you can do things like Ok(entry) = lookup_table.get(5)

to do an exhaustive pattern match that Roc's compiler will know is valid thanks to compile-time evaluation. This avoids runtime bounds checks that non-constant tables might have to pay for.

Consider Wrapping Arithmetic

In Roc, a + b

desugars to a.plus(b)

. For an integer like U64

, U64.plus

includes an overflow check, whereas plus_wrap

does not. Although plus

is the safer default, overflow checks have a runtime cost, as CPUs can do wrapping integer arithmetic in one instruction. When Roc's compiler can prove that overflow won't occur, both plus

and plus_wrap

will optimize to the same instructions, but when it can't, you may get a speedup by choosing plus_wrap

.

Room for Improvement

I'm sure there are still opportunities to improve roc-deflate's performance—especially on x64 CPUs—as well as the optimization pipeline in Roc's compiler.

At this point I feel reasonably confident that all the low-hanging fruit has been picked, but if anyone wants to try it out for themselves, here's the script I ran to set up and run the benchmark suite on Linux. (Note that Claude Opus 5.5 generated this script, and I haven't vetted it for security; run it outside a sandbox at your own risk!) It pins both Roc compiler revisions and roc-deflate versions, so you'll want to adjust those commits if you're trying out potential improvements.

For my part, I'll be focusing on the upcoming Roc 0.1.0 release from now until it's out. It's been eight years in the making, so I'll be very happy for us to finally have a numbered release! If you'd like to learn more about Roc, check out roc-lang.org or come say hi on Roc Zulip!

Original source: rtfeldman.com

Technology