<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Frank DENIS random thoughts.</title>
  <link href="https://00f.net/atom.xml" rel="self"/>
  <link href="https://00f.net/"/>
  <updated>2026-07-17T01:09:04+02:00</updated>
  <id>https://00f.net</id>
  
  <author>
    <name>Frank Denis (Jedi/Sector One)</name>
  </author>
  
  
  <entry>
    <title>AES gets swizzled</title>
    <link href="https://00f.net/2026/07/16/aes-with-simd-swizzles/"/>
   <updated>2026-07-16T00:00:00+02:00</updated>
   <id>https://00f.net/2026/07/16/aes-with-simd-swizzles</id>
   <content type="html">&lt;p&gt;There’s an old problem with AES when implemented in software: it’s either slow or insecure.&lt;/p&gt;

&lt;p&gt;AES has a state of sixteen bytes, and a round has four steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SubBytes&lt;/code&gt; replaces every byte using the AES S-Box.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShiftRows&lt;/code&gt; moves bytes to different columns.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MixColumns&lt;/code&gt; combines the four bytes in each column.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AddRoundKey&lt;/code&gt; XORs another sixteen-byte value.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SubBytes&lt;/code&gt; is the annoying part, because it applies a random-looking permutation to every byte:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SBox(0x00) = 0x63
SBox(0x53) = 0xed
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;An obvious way to implement that is by using lookup tables.&lt;/p&gt;

&lt;p&gt;But here’s the problem: the lookup indices are secret data, and accessing a cache line that was just accessed and is still in the cache is slightly faster than accessing other addresses.&lt;/p&gt;

&lt;p&gt;Taking advantage of this, an adversary on the same machine can learn information about the secret indices. For example, DJB &lt;a href=&quot;https://cr.yp.to/antiforgery/cachetiming-20050414.pdf&quot;&gt;recovered AES keys from remote timings&lt;/a&gt;, and Osvik, Shamir, and Tromer demonstrated &lt;a href=&quot;https://eprint.iacr.org/2005/271&quot;&gt;cross-process attacks against OpenSSL and disk encryption&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That was mostly solved on modern mobile, desktop, and server CPUs by adding AES instructions.&lt;/p&gt;

&lt;p&gt;But old is new again. With new platforms such as WebAssembly, even when running on such CPUs, applications can’t use AES instructions, so they have to reimplement AES themselves. Sigh.&lt;/p&gt;

&lt;p&gt;A common way to avoid using lookup tables is bitslicing: using a circuit of logical operations applied to a different representation of the AES state, where all the bits expected to follow the same circuit are packed together in a register.&lt;/p&gt;

&lt;p&gt;It works very well in hardware, but in software, performance is generally still not great, especially compared to what CPU AES instructions can do.&lt;/p&gt;

&lt;p&gt;But there’s a third option that’s surprisingly not well known and was originally described by Mike Hamburg in &lt;a href=&quot;https://www.shiftleft.org/papers/vector_aes/vector_aes.pdf&quot;&gt;Accelerating AES with Vector Permute Instructions&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;a-lookup-table-inside-a-register&quot;&gt;A lookup table inside a register&lt;/h2&gt;

&lt;p&gt;Modern CPUs, even when accessed via WebAssembly, include something nice: SIMD registers that contain 16 bytes or more.&lt;/p&gt;

&lt;p&gt;A lot of instructions can be used with such registers, but a very common one, which (oh, joy!) is even accessible in WebAssembly, treats one 16-byte vector as a table and another as sixteen selectors:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;table     = [t0, t1, t2, ... t15]
selectors = [ 3,  9,  0, ...   7]
result    = [t3, t9, t0, ...  t7]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A single instruction effectively performs sixteen lookups in parallel. The table is kept in a register, so there are no memory lookups, and (barring microarchitectural vulnerabilities) no side channels.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/posts/simd-aes-lookups.svg&quot; alt=&quot;A secret byte selects an AES S-Box address in memory, while a SIMD byte permutation selects lanes in a register loaded from a fixed address.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is something that all CPUs with SIMD instructions support. On x86, it’s called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PSHUFB&lt;/code&gt;, on ARM, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TBL&lt;/code&gt;, and WebAssembly has a corresponding instruction called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i8x16.swizzle&lt;/code&gt;, which any sane compiler can map directly to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PSHUFB&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TBL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When selectors are between 0 and 15, the behavior is as expected, and the same across all targets.&lt;/p&gt;

&lt;p&gt;Values outside that range are target-specific, which is important for WebAssembly (more about that later).&lt;/p&gt;

&lt;h2 id=&quot;turning-256-values-into-16-by-16&quot;&gt;Turning 256 values into 16 by 16&lt;/h2&gt;

&lt;p&gt;The AES S-Box isn’t actually random. It’s defined as:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;S(b) = A(inverse(b)) XOR 0x63
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The byte is inverted in the AES finite field. Then a fixed linear bit transformation &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt; and the constant &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x63&lt;/code&gt; are applied. And the inverse of zero is defined as zero.&lt;/p&gt;

&lt;p&gt;AES uses a finite field with 256 elements, commonly written &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GF(2^8)&lt;/code&gt;. And the same field can be represented as a quadratic extension of a 16-element field:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GF(2^8) is isomorphic to GF(2^4)[t] / (t^2 + t + zeta)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With these binary encodings, field addition in both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GF(2^8)&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GF(2^4)&lt;/code&gt; is bitwise XOR.&lt;/p&gt;

&lt;p&gt;Here &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zeta&lt;/code&gt; is chosen so that the quadratic polynomial is irreducible.&lt;/p&gt;

&lt;p&gt;And here’s something interesting:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;256 = 16 * 16
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This is just a reversible change of coordinates.&lt;/p&gt;

&lt;p&gt;And in the new representation, one byte becomes a pair of encoded four-bit field components, while addition and multiplication keep working as expected.&lt;/p&gt;

&lt;p&gt;A linear input transformation produces those components:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;lo = input &amp;amp; 0x0f
hi = input &amp;gt;&amp;gt; 4

encoded = lookup16(input_map_lo, lo) XOR lookup16(input_map_hi, hi)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;See? The low and high nibble contributions can be looked up separately because the transform is linear.&lt;/p&gt;

&lt;p&gt;With two permutations, we can transform all sixteen AES state bytes.&lt;/p&gt;

&lt;h2 id=&quot;inversion-with-small-register-tables&quot;&gt;Inversion with small register tables&lt;/h2&gt;

&lt;p&gt;Hamburg’s nested-inversion construction reduces the nonlinear part of the field inverse to five small lookups:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;k = low_nibble(encoded)
i = high_nibble(encoded)
j = k XOR i

ak  = lookup16(inverse_scaled, k)
iak = lookup16(inverse, i) XOR ak
jak = lookup16(inverse, j) XOR ak
io  = lookup16(inverse, iak) XOR j
jo  = lookup16(inverse, jak) XOR i
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The stages use scaled and skewed representations, so treating every value as an ordinary nibble in one fixed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GF(2^4)&lt;/code&gt; basis gives incorrect results.&lt;/p&gt;

&lt;p&gt;So, two output tables apply the remaining factors, return to the normal AES byte basis, and apply the linear part of the S-Box affine map:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;a = lookup16(output_u, io) XOR lookup16(output_t, jo)

a XOR 0x63 = AES_SBOX[input]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With the target-specific infinity handling described below, we get the correct S-Box value for every possible input byte. And we only need 16-element lookup tables. Each fits in a SIMD register. You see where this is going.&lt;/p&gt;

&lt;h2 id=&quot;folding-the-rest-of-the-round&quot;&gt;Folding the rest of the round&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MixColumns&lt;/code&gt; is evaluated on the constant-free S-Box value &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a = A(inverse(input))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It needs both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a&lt;/code&gt; multiplied by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x02&lt;/code&gt; in the AES field (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3a&lt;/code&gt; is also needed, but that’s just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a + 2a&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Since that operation is linear, a second pair of output tables can produce the doubled value directly:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;a2 = lookup16(output2_u, io) XOR lookup16(output2_t, jo)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Four fixed permutations gather the bytes after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShiftRows&lt;/code&gt; and arrange the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MixColumns&lt;/code&gt; contributions:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;m0 = permute(a2,       mix0)
m1 = permute(a XOR a2, mix1)
m2 = permute(a,        mix2)
m3 = permute(a,        mix3)

output = m0 XOR m1 XOR m2 XOR m3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;How about the affine constant &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x63&lt;/code&gt; we talked about earlier? Turns out that we can omit it.&lt;/p&gt;

&lt;p&gt;A vector containing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x63&lt;/code&gt; in every byte is unchanged by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ShiftRows&lt;/code&gt; and survives &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MixColumns&lt;/code&gt; because every output row’s coefficients XOR to one:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;0x02 XOR 0x03 XOR 0x01 XOR 0x01 = 0x01
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Just one final vector XOR restores &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x63&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can finally &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AddRoundKey&lt;/code&gt;, which is a simple XOR.&lt;/p&gt;

&lt;p&gt;To do all this, we only need 15 byte permutations:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Input transformation:          2
Nested inversion:              5
S-Box output and its double:   4
ShiftRows and MixColumns:      4
                              --
Total:                        15
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;swizzling-infinity&quot;&gt;Swizzling infinity&lt;/h2&gt;

&lt;p&gt;The inversion contains denominators that can be zero. So we need some kind of out-of-range selector to represent infinity.&lt;/p&gt;

&lt;p&gt;The inverse tables map a zero denominator to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x80&lt;/code&gt;. XORing that with a nibble can produce anything from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x80&lt;/code&gt; through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x8f&lt;/code&gt;, but these values all represent the same infinity marker: bit 7 is set, while the low nibble doesn’t matter. When one of them selects from the next 16-byte table, the byte permutation returns zero.&lt;/p&gt;

&lt;p&gt;And as mentioned previously, different targets treat out-of-range selectors differently.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Primitive&lt;/th&gt;
      &lt;th&gt;Out-of-range selector behavior&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;x86 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PSHUFB&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;If selector bit 7 is set, the result is zero; otherwise the low nibble selects&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AArch64 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TBL&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Every index above 15 returns zero&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;WebAssembly strict SIMD&lt;/td&gt;
      &lt;td&gt;Every index above 15 returns zero&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;WebAssembly relaxed SIMD&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x10&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x7f&lt;/code&gt; may return zero or use the low nibble; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x80&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xff&lt;/code&gt; returns zero&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A selector between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;15&lt;/code&gt; or between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x80&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xff&lt;/code&gt; produces the same output everywhere, even in WebAssembly.&lt;/p&gt;

&lt;p&gt;And the circuit described above only produces selectors in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x00&lt;/code&gt; through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x0f&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x80&lt;/code&gt; through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x8f&lt;/code&gt;. So, we’re safe!&lt;/p&gt;

&lt;p&gt;The default WebAssembly “strict” SIMD mode doesn’t map well to x86 instructions, so the generated code is pretty inefficient.&lt;/p&gt;

&lt;p&gt;Fortunately, WebAssembly later introduced a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;relaxed-simd&lt;/code&gt; variant of the instruction that marks selectors in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x10&lt;/code&gt; through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0x7f&lt;/code&gt; range as “don’t-care” values and lowers directly to a CPU instruction.&lt;/p&gt;

&lt;p&gt;It’s a little sad that WebAssembly support is fragmented and relaxed SIMD isn’t supported everywhere yet, most notably in Safari. At least server runtimes such as Wasmer, WasmEdge, Wasmtime, and WAVM have supported it by default for a long time. And Safari will eventually catch up.&lt;/p&gt;

&lt;p&gt;Is this technique worth it compared to lookup tables and bitslicing? It is. Compared to lookup tables, it guarantees constant-time execution. And compared to bitslicing, it doesn’t require bit-level representation changes and introduces less register pressure.&lt;/p&gt;

&lt;p&gt;Most importantly, it’s a very good match for how some new AES-based ciphers such as AEGIS and HiAE operate. And we can finally have ciphers with half-decent performance on WebAssembly rumtimes without the WASI-Crypto extensions.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>The best WebAssembly runtime may still be no runtime at all</title>
    <link href="https://00f.net/2026/07/08/webassembly-compilation-to-c-2026/"/>
   <updated>2026-07-08T00:00:00+02:00</updated>
   <id>https://00f.net/2026/07/08/webassembly-compilation-to-c-2026</id>
   <content type="html">&lt;p&gt;In 2023, I wrote that &lt;a href=&quot;/2023/12/11/webassembly-compilation-to-c/&quot;&gt;the best WebAssembly runtime may be no runtime at all&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To summarize: if you already have a WebAssembly module, translating it to C and compiling that C with a normal native compiler can be surprisingly hard to beat.&lt;/p&gt;

&lt;p&gt;I expected that argument to get weaker over time, because WebAssembly runtimes keep improving. Their compilers have better register allocators, better lowering, better support for newer WebAssembly instructions, and more deployment polish than a small transpiler can reasonably have.&lt;/p&gt;

&lt;p&gt;Then I ran the 2026 benchmark.&lt;/p&gt;

&lt;p&gt;The 2026 &lt;a href=&quot;/2026/06/23/webassembly-runtimes-2026/&quot;&gt;WebAssembly benchmark&lt;/a&gt; has the full table for other runtimes.&lt;/p&gt;

&lt;p&gt;And the WebAssembly-to-C path performed very well, but was beaten by new runtimes implementing the “wide arithmetic” WebAssembly proposal.&lt;/p&gt;

&lt;p&gt;I was curious to see how the WebAssembly-to-C approach would work with if that proposal was implemented.&lt;/p&gt;

&lt;p&gt;So, let’s compare:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer 7.1.0&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime 46.0.0&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;WABT &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt;, I added support for wide arithmetic, which was surprisingly trivial to implement. You can find my fork &lt;a href=&quot;https://github.com/dip-proto/wabt&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The generated C code simply uses compiler carry intrinsics and C’s 128-bit integer type.&lt;/p&gt;

&lt;p&gt;Note that I compiled the generated C with WABT’s Segue memory mode enabled, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zig cc -O3 -march=native&lt;/code&gt;, on the exact same libsodium benchmark suite as the previous runtime post.&lt;/p&gt;

&lt;p&gt;Every build uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128+wide_arithmetic&lt;/code&gt; feature set.&lt;/p&gt;

&lt;p&gt;Oh, and before you ask: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; supports memory protection using guard pages, like other runtimes.&lt;/p&gt;

&lt;h2 id=&quot;the-speed-numbers&quot;&gt;The speed numbers&lt;/h2&gt;

&lt;p&gt;The numbers below are slowdowns relative to the native libsodium build. A value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.25x native&lt;/code&gt; means the benchmark took 25% more time than native code.&lt;/p&gt;

&lt;p&gt;Lower is better.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2c2026/speed.png&quot; alt=&quot;wasm2c with wide arithmetic, ranked by geomean slowdown&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The median is a tie with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt;. But the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; executable used 0.887x as much time as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; on the geomean, and 0.814x as much as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The dumb C path survived the feature upgrade. Once the new arithmetic instructions were added, a normal C compiler could again produce code competitive with dedicated WebAssembly compilers.&lt;/p&gt;

&lt;h2 id=&quot;memory-usage&quot;&gt;Memory usage&lt;/h2&gt;

&lt;p&gt;I also measured peak resident set size with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/bin/time -v&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is cold process RSS for one command invocation, using the median of fifteen runs. It includes the runtime executable, its startup state, JIT or compilation machinery used by that command, and anything else the process maps while running the benchmark.&lt;/p&gt;

&lt;p&gt;Note that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; have a fixed cost that can be amortized if a service keeps the runtime alive and runs many modules or many calls.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2c2026/memory.png&quot; alt=&quot;Cold process memory, median of 15 runs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Most of that difference is fixed engine overhead. The module itself costs very little.&lt;/p&gt;

&lt;p&gt;If I subtract the RSS of an empty WASI module run through the same path, with a 64 MiB maximum linear memory like the benchmark build, only a couple of MiB remain:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2c2026/memory-extra.png&quot; alt=&quot;What the module itself costs&quot; /&gt;&lt;/p&gt;

&lt;p&gt;At run time, with the WebAssembly code directly compiled to C, then to executable code, there’s no engine process to bring along.&lt;/p&gt;

&lt;h2 id=&quot;no-runtime-is-still-a-good-choice&quot;&gt;No runtime is still a good choice&lt;/h2&gt;

&lt;p&gt;The generated output is portable C.&lt;/p&gt;

&lt;p&gt;So, the deployment target no longer has to be “a platform with a WebAssembly runtime”. It can be any platform with a C compiler and enough libc support for whatever the module imports.&lt;/p&gt;

&lt;p&gt;It also means the compiler is a choice.&lt;/p&gt;

&lt;p&gt;You can compile the generated C with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clang&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gcc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But you can also take advantage of modern C toolchains such as &lt;a href=&quot;https://fil-c.org/&quot;&gt;Fil-C&lt;/a&gt; to get memory safety on wasm64, or on wasm32 without depending on virtual-memory guard pages.&lt;/p&gt;

&lt;p&gt;Or you can use a formally verified compiler such as &lt;a href=&quot;https://compcert.org/&quot;&gt;CompCert&lt;/a&gt; for high assurance code generation.&lt;/p&gt;

&lt;p&gt;I didn’t benchmark those toolchains. The point is that WebAssembly-to-C gives you that option, while a dedicated runtime doesn’t.&lt;/p&gt;

&lt;p&gt;This is also why I remain impressed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; also implements &lt;a href=&quot;https://wasix.org/&quot;&gt;WASIX&lt;/a&gt;, which fills many of the POSIX-shaped gaps that plain WASI still leaves open. So, if your goal is to run existing applications in WebAssembly with fewer rewrites, this is the way to go, not WebAssembly-to-C or other options.&lt;/p&gt;

&lt;p&gt;And &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; implements the “Component Model”, that improves code separation and gives applications a typed interface boundary for composing pieces of WebAssembly. I have an intuition that the same thing can be achieved using WebAssembly-to-C by compiling components separately, but I didn’t think about it much.&lt;/p&gt;

&lt;p&gt;Regardless WebAssembly-to-C is the wrong answer if you need dynamic loading of untrusted code without precompilation, runtime-level policy, preemption, fuel, component-model machinery, or one engine reused across many tenants.&lt;/p&gt;

&lt;p&gt;It’s also the wrong answer if your deployment model depends on shipping one portable &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.wasm&lt;/code&gt; artifact and letting the destination machine choose how to run it.&lt;/p&gt;

&lt;p&gt;But if you control the WebAssembly module, can precompile it, and don’t need runtime services, WebAssembly-to-C is very close to a no-brainer, especially with the addition of wide arithmetic.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Calling everything AI-generated is lazy</title>
    <link href="https://00f.net/2026/06/25/stop-calling-everything-ai-generated/"/>
   <updated>2026-06-25T00:00:00+02:00</updated>
   <id>https://00f.net/2026/06/25/stop-calling-everything-ai-generated</id>
   <content type="html">&lt;p&gt;The new lazy comment’s “AI-generated”.&lt;/p&gt;

&lt;p&gt;On Hacker News, every thread about a programming language that isn’t Rust eventually gets a Rust comment. It doesn’t matter if the topic’s C, Zig, Go, PHP, Elixir, or a database. Somebody will find a way to turn the discussion into Rust.&lt;/p&gt;

&lt;p&gt;We now have the same reflex for writing.&lt;/p&gt;

&lt;p&gt;Someone publishes a blog post. Maybe something a shower thought. Maybe just a weird thing they spent the weekend trying to understand.&lt;/p&gt;

&lt;p&gt;Instead of pointing to a wrong number, a missing test, a wrong claim, or saying thank you, somebody just writes “sounds AI-generated”.&lt;/p&gt;

&lt;p&gt;That isn’t criticism. It gives the author nothing to fix. No sentence. No claim. No result. Just a little accusation dropped in the middle of the room. Just sending bad vibes for no reason.&lt;/p&gt;

&lt;p&gt;And it lands on real people.&lt;/p&gt;

&lt;p&gt;A lot of personal blogs aren’t content businesses. They’re notes written after work, or on a weekend, or at 4am, while the author’s still excited or annoyed enough to explain what they just found. The research may’ve taken days. The writing may’ve taken hours. The reward’s usually nothing. Maybe one useful comment. Maybe one email years later. Maybe just knowing that the next person won’t lose the same afternoon.&lt;/p&gt;

&lt;p&gt;Calling that “AI-generated” isn’t harmless.&lt;/p&gt;

&lt;p&gt;The garbage sites that actually publish AI slop don’t care. There’s nobody behind them feeling insulted. The pipeline doesn’t read Hacker News. It doesn’t get discouraged. It doesn’t wonder whether publishing was a mistake. It scrapes, generates, publishes, and repeats.&lt;/p&gt;

&lt;p&gt;The person writing a blog does read it.&lt;/p&gt;

&lt;p&gt;I’m French. English isn’t my native language.&lt;/p&gt;

&lt;p&gt;When I write, I don’t first think “how would a native speaker phrase this?” I think about the thing I just did or the idea I want to get out before the excitement’s gone.&lt;/p&gt;

&lt;p&gt;And I use a computer, so yes, I use tools.&lt;/p&gt;

&lt;p&gt;I utterly hate AI-generated content. I don’t want machines to replace personal writing with synthetic filler.&lt;/p&gt;

&lt;p&gt;But AI as an assistant is different.&lt;/p&gt;

&lt;p&gt;Checking. Verifying. Proofreading. Helping me say in English what I already know I want to say. &lt;em&gt;That&lt;/em&gt; is useful.&lt;/p&gt;

&lt;p&gt;I use Apple Intelligence proofreading on my Mac all the time. I ask ChatGPT to fix my English. It fixes grammar. It removes very French sentence structures. It rewrites everything into something that looks a bit too clean. Do you really think I can’t see it too?&lt;/p&gt;

&lt;p&gt;AI-assisted prose has patterns. Any idiot can recognizing its stinky smell.&lt;/p&gt;

&lt;p&gt;But a spell checker doesn’t make a post fake. Grammarly didn’t make old posts fake. A machine helping a non-native speaker write more readable English doesn’t make the work fake.&lt;/p&gt;

&lt;p&gt;The work’s the part before the sentence.&lt;/p&gt;

&lt;p&gt;Did I write from actual experience? Did I put my name on the result and accept being wrong in public? This is what should matter.&lt;/p&gt;

&lt;p&gt;That’s also why the drive-by accusation is so annoying.&lt;/p&gt;

&lt;p&gt;Real AI slop exists. It’s everywhere and it’s awful. But as a filter, “this sounds like AI” is just fkcing annoying.&lt;/p&gt;

&lt;p&gt;Mostly, it detects fluency. That’s exactly what non-native writers use tools for. It punishes people for making their English easier to read. It pushes them toward a stupid choice: write worse and sound human, or write better and become suspicious.&lt;/p&gt;

&lt;p&gt;That sucks.&lt;/p&gt;

&lt;p&gt;If a post’s wrong, say what’s wrong. If a chart’s bogus, show the bug. If a claim’s too strong, argue with the claim. If a paragraph’s clearly hallucinated, quote it.&lt;/p&gt;

&lt;p&gt;But a drive-by “AI-generated” comment’s just a status signal. It says “I noticed the current thing.” It doesn’t improve the discussion. It doesn’t protect anyone. It doesn’t help the author. It only makes publishing feel worse.&lt;/p&gt;

&lt;p&gt;And publishing personal writing already has a cost.&lt;/p&gt;

&lt;p&gt;You expose how you think. You expose what you don’t know. If you write in a second language, you also expose every awkward sentence. There’s always the small fear that the post’s too rough, too direct, too French, too wrong.&lt;/p&gt;

&lt;p&gt;Proofreading tools reduce that friction. They let me share the technical part instead of spending all my energy fighting articles and prepositions.&lt;/p&gt;

&lt;p&gt;If that makes some sentences look assisted, so be it.&lt;/p&gt;

&lt;p&gt;I care more about whether the content’s useful than whether every comma proves I typed it manually.&lt;/p&gt;

&lt;p&gt;The annoying part’s that people keep saying they want the human web back. They want personal blogs, independent notes, small discoveries, weird experiments, people writing outside platforms.&lt;/p&gt;

&lt;p&gt;Then the same people make it unpleasant to publish any of that.&lt;/p&gt;

&lt;p&gt;Haters are gonna hate. Fine.&lt;/p&gt;

&lt;p&gt;But this particular kind of hate costs something. The AI farms survive the comments. Humans may not.&lt;/p&gt;

&lt;p&gt;So no, I won’t apologize for using proofreading tools.&lt;/p&gt;

&lt;p&gt;If you want to fight AI slop, fight empty work. Fight fake expertise. Fight articles with no author, no experience, no accountability, and no point.&lt;/p&gt;

&lt;p&gt;Don’t make humans prove they’re human every time they write a decent sentence.&lt;/p&gt;

&lt;p&gt;That’s how you get fewer humans writing.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Performance of WebAssembly runtimes in 2026</title>
    <link href="https://00f.net/2026/06/23/webassembly-runtimes-2026/"/>
   <updated>2026-06-23T00:00:00+02:00</updated>
   <id>https://00f.net/2026/06/23/webassembly-runtimes-2026</id>
   <content type="html">&lt;p&gt;I wanted to know if WebAssembly runtimes are getting faster.&lt;/p&gt;

&lt;p&gt;This is a follow-up to the earlier libsodium WebAssembly benchmarks from &lt;a href=&quot;/2019/04/09/benchmarking-webassembly-using-libsodium/&quot;&gt;2019&lt;/a&gt;, &lt;a href=&quot;/2021/02/22/webassembly-runtimes-benchmarks/&quot;&gt;2021&lt;/a&gt; and &lt;a href=&quot;/2023/01/04/webassembly-benchmark-2023/&quot;&gt;2023&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Not “does the newest version beat native code in one microbenchmark?”, and not “which runtime has the prettiest benchmark chart?”, but something more boring and more useful:&lt;/p&gt;

&lt;p&gt;If I take the same C crypto code, compile it to WebAssembly, and run it on the latest runtime, a runtime from one year ago, and a runtime from two years ago, are things actually improving?&lt;/p&gt;

&lt;p&gt;So I benchmarked libsodium on WebAssembly runtimes released around June 2024, June 2025, and June 2026.&lt;/p&gt;

&lt;p&gt;The short version:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasmer&lt;/code&gt; is the best performer, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; are close.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt; has the best optimizer, and is able to generate very fast code out from baseline, portable WebAssembly&lt;/li&gt;
  &lt;li&gt;The new WebAssembly &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; instructions are a big deal for crypto code when runtimes support them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-i-measured&quot;&gt;What I measured&lt;/h2&gt;

&lt;p&gt;The test program is libsodium’s benchmark suite, built from libsodium commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8e3be8615ba6adcd7babaecf5e76f516890ba5fb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I built one native baseline and several WebAssembly variants:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;native x86-64, compiled with Zig using the local CPU target&lt;/li&gt;
  &lt;li&gt;plain WebAssembly&lt;/li&gt;
  &lt;li&gt;WebAssembly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;WebAssembly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simd128&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;WebAssembly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simd128&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the native reference, libsodium was built with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-Dcpu=native&lt;/code&gt;. For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt;, the generated C was compiled with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zig cc -O3 -march=native&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt;, I used AOT mode: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wamrc&lt;/code&gt; compiled each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.wasm&lt;/code&gt; file to an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.aot&lt;/code&gt; file, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iwasm&lt;/code&gt; ran the resulting AOT file. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wamrc&lt;/code&gt; doesn’t accept &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--cpu=native&lt;/code&gt;, so I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--target=x86_64 --cpu=x86-64-v4 --opt-level=3&lt;/code&gt;, which matches the host’s available x86-64 feature level and works across the WAMR versions that could compile these modules.&lt;/p&gt;

&lt;p&gt;The native command was:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;native &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The WebAssembly commands were the same shape, with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm32-wasi&lt;/code&gt; target and the feature-specific CPU strings:&lt;/p&gt;

&lt;div class=&quot;language-sh highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;lime1 &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;lime1+simd128 &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
zig build &lt;span class=&quot;nt&quot;&gt;-Denable_benchmarks&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Dtarget&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;wasm32-wasi &lt;span class=&quot;nt&quot;&gt;-Doptimize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;ReleaseFast &lt;span class=&quot;nt&quot;&gt;-Dcpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;lime1+simd128+wide_arithmetic &lt;span class=&quot;nt&quot;&gt;-Diterations&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The host was an AMD Ryzen AI 9 HX 470 with 12 cores and 24 threads. CPU boost was disabled and the maximum CPU frequency was 2 GHz. The OS was Linux 7.1.0-rc7, and Zig was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.17.0-dev.948+e949341b7&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The numbers below are the geometric mean of per-benchmark slowdowns relative to the native build. Lower is better. A value of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.0&lt;/code&gt; means “twice as slow as native” on this machine.&lt;/p&gt;

&lt;p&gt;I used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ITERATIONS=3&lt;/code&gt;, so the very small libsodium tests are noisy and quantized. Rows reporting zero time were excluded from the aggregate.&lt;/p&gt;

&lt;h2 id=&quot;versions&quot;&gt;Versions&lt;/h2&gt;

&lt;p&gt;For every runtime except &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt;, I used the latest stable release available on June 23, 2026, plus a stable release from roughly one year earlier and one from roughly two years earlier.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Runtime&lt;/th&gt;
      &lt;th&gt;2024&lt;/th&gt;
      &lt;th&gt;2025&lt;/th&gt;
      &lt;th&gt;2026&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/oven-sh/bun/releases/tag/bun-v1.1.16&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.1.16&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/oven-sh/bun/releases/tag/bun-v1.2.17&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.2.17&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/oven-sh/bun/releases/tag/bun-v1.3.14&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3.14&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://nodejs.org/dist/v22.3.0/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;22.3.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://nodejs.org/dist/v24.2.0/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;24.2.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://nodejs.org/dist/v26.3.1/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;26.3.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasm-micro-runtime/releases/tag/WAMR-2.1.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.1.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasm-micro-runtime/releases/tag/WAMR-2.3.1&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.3.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasm-micro-runtime/releases/tag/WAMR-2.4.4&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.4.4&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WABT wasm2c&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WebAssembly/wabt/releases/tag/1.0.35&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.35&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WebAssembly/wabt/releases/tag/1.0.37&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.37&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WebAssembly/wabt/releases/tag/1.0.41&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.0.41&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WasmEdge/WasmEdge/releases/tag/0.14.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.14.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WasmEdge/WasmEdge/releases/tag/0.14.1&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.14.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WasmEdge/WasmEdge/releases/tag/0.17.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.17.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wasmerio/wasmer/releases/tag/v4.3.2&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4.3.2&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wasmerio/wasmer/releases/tag/v6.0.1&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6.0.1&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wasmerio/wasmer/releases/tag/v7.1.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7.1.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasmtime/releases/tag/v22.0.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;22.0.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasmtime/releases/tag/v34.0.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;34.0.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/bytecodealliance/wasmtime/releases/tag/v46.0.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;46.0.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n/a&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n/a&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/WAVM/WAVM/releases/tag/nightly/2026-04-05&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nightly/2026-04-05&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wazero&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wazero/wazero/releases/tag/v1.7.3&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.7.3&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wazero/wazero/releases/tag/v1.9.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.9.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;&lt;a href=&quot;https://github.com/wazero/wazero/releases/tag/v1.12.0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.12.0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt; is awkward to compare historically. The old available nightly collapsed to a 2022 binary for both the 2024 and 2025 slots, and that binary refused to run on this machine. I only kept the 2026 nightly.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.1.0&lt;/code&gt;, the selected 2024 release, installed fine but its AOT compiler failed on these Zig-generated modules with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;invalid WASM stack data type&lt;/code&gt;. I kept the version in the matrix, but didn’t include an aggregate for it.&lt;/p&gt;

&lt;h2 id=&quot;baseline-webassembly&quot;&gt;Baseline WebAssembly&lt;/h2&gt;

&lt;p&gt;This is the plain WebAssembly build, without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, SIMD, or wide arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/baseline.png&quot; alt=&quot;Baseline WebAssembly slowdown by release year&quot; /&gt;&lt;/p&gt;

&lt;p&gt;There isn’t one universal trend.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; steadily improved: 2.67x native in 2024, 2.54x in 2025, 2.41x in 2026. It got faster every year, and the gains land in the tenths place, above the noise.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt; also improved slowly, from 8.60x native to 7.95x native.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wazero&lt;/code&gt; was basically flat: 4.84x, 4.70x, 4.72x native. No real movement over two years.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; in AOT mode was already fast in 2025 and stayed there in 2026: 1.59x native, then 1.57x native, the same within this benchmark’s noise. I don’t have a complete 2024 WAMR number because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.1.0&lt;/code&gt; couldn’t compile these modules.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; regressed in the 2025 release I tested, then recovered in 2026. The 2026 baseline barely beats 2024.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; improved modestly in 2026. It remains one of the best options if ahead-of-time translation to native C is acceptable for your deployment model.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; is the outlier. Its 2024 and 2025 results were far behind, but the 2026 result is about three times faster than the 2025 result. It’s still slower than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt; on this benchmark, but the direction is excellent.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt; is fast too, but its command-line behavior changed enough to matter. My first &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.17.0&lt;/code&gt; run accidentally used interpreter mode for compiled modules and looked catastrophically slow. Running the compiled modules with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--run-mode=aot&lt;/code&gt; fixed it: the 2026 baseline was 1.74x native, between the 2024 and 2025 baseline results.&lt;/p&gt;

&lt;h2 id=&quot;best-supported-build-by-year&quot;&gt;Best supported build by year&lt;/h2&gt;

&lt;p&gt;The baseline table is useful because it compares the same WebAssembly target everywhere.&lt;/p&gt;

&lt;p&gt;But if you’re choosing a runtime for your own deployment, you probably care about the fastest build that runtime can actually run.&lt;/p&gt;

&lt;p&gt;So for each runtime and year, I also selected the best complete result among the supported builds: baseline, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128+wide_arithmetic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/best-by-year.png&quot; alt=&quot;Best supported build by release year&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Looks similar to the previous graph, except for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; that really benefit from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ranked by the best supported build, the complete current-year results are:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/rank-2026.png&quot; alt=&quot;2026 releases, best supported build, ranked&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;cpu-feature-variants&quot;&gt;CPU feature variants&lt;/h2&gt;

&lt;p&gt;The WebAssembly feature story is more interesting than the year-to-year runtime story.&lt;/p&gt;

&lt;p&gt;For the 2026 releases, these were the aggregate slowdowns:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/wasm2026/features-2026.png&quot; alt=&quot;CPU feature variants across 2026 releases&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simd128&lt;/code&gt; alone aren’t magic here. Sometimes they help, sometimes they hurt, and sometimes the difference is lost in benchmark noise.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; is different.&lt;/p&gt;

&lt;p&gt;Only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; could run the full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; build among the complete stable rows I tested. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; rejected it with unsupported opcode &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xfc13&lt;/code&gt;. But when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; worked, it was the biggest speedup in the whole experiment:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime 46.0.0&lt;/code&gt;: 2.41x native without it, 1.46x native with it.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer 7.1.0&lt;/code&gt;: 2.08x native without it, 1.33x native with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the kind of change I like. A lot of libsodium’s expensive operations are arithmetic-heavy. If the WebAssembly ISA can express that arithmetic directly, the runtime has much less work to rediscover what the C compiler already knew.&lt;/p&gt;

&lt;h2 id=&quot;failures&quot;&gt;Failures&lt;/h2&gt;

&lt;p&gt;Most runs completed cleanly, but not all of them.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun 1.2.17&lt;/code&gt; failed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;box_easy&lt;/code&gt; in the baseline build. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun 1.1.16&lt;/code&gt; failed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2i&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt; builds.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node 22.3.0&lt;/code&gt; originally failed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2i&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2id&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_scrypt&lt;/code&gt; in the baseline, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt; builds. The failures weren’t fixed by increasing Node’s JavaScript heap or stack settings. They were fixed by giving the Wasm modules an explicit maximum linear memory. With the baseline build, a 1024-page maximum, or 64 MiB, made &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2i&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_argon2id&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_scrypt&lt;/code&gt; complete. But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pwhash_scrypt&lt;/code&gt; failed with 512 pages and segfaulted again at 1536 pages and above, so this appears to be a V8 memory-mode threshold rather than a simple “more memory is better” setting.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.1.0&lt;/code&gt;, the 2024 slot, couldn’t compile even the baseline modules in AOT mode. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR 2.3.1&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.4.4&lt;/code&gt; compiled and ran the baseline, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lime1+simd128&lt;/code&gt; builds, but not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those failures were excluded from the aggregate. So were benchmark rows with a zero reported median.&lt;/p&gt;

&lt;h2 id=&quot;so-are-runtimes-getting-faster&quot;&gt;So, are runtimes getting faster?&lt;/h2&gt;

&lt;p&gt;Some of them are.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; is the cleanest yes: it got faster every year in this benchmark, by a little each time.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt; is also a yes, but the slope is gentle.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; is a loud yes between 2025 and 2026. It still has a lot of ground to cover for this workload, but the improvement is too large to ignore.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wazero&lt;/code&gt; is mostly flat.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAMR&lt;/code&gt; is also mostly flat between the versions that worked here, but “flat” at about 1.4x to 1.6x native is a very good place to be.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; is mixed if you only look at the baseline, but the 2026 release supporting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; changes the practical answer for crypto code. With that feature enabled, it was the fastest complete 2026 result I could compare across a normal current release.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wasm2c&lt;/code&gt; remains good. If you can translate WebAssembly to C ahead of time and compile it for the host, it’s hard to beat.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WAVM&lt;/code&gt; produced the fastest 2026 baseline number, but I don’t have a fair 2024 or 2025 comparison.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt; remains excellent once it’s forced into AOT mode. The accidental interpreter-mode run was a good reminder that command-line defaults are part of the benchmark, too.&lt;/p&gt;

&lt;h2 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h2&gt;

&lt;p&gt;If you run CPU-heavy cryptography in WebAssembly, runtime choice still matters a lot.&lt;/p&gt;

&lt;p&gt;The spread between the fastest complete current result and the slowest current result is large: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; was 1.33x native, while current &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; baseline was 8.77x native.&lt;/p&gt;

&lt;p&gt;Feature support matters too. The same runtime can move from “pretty good” to “surprisingly close to native” when the WebAssembly module can use better arithmetic instructions.&lt;/p&gt;

&lt;p&gt;The comforting part is that the mainstream runtimes aren’t standing still. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmtime&lt;/code&gt; improved steadily. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bun&lt;/code&gt; made a huge jump. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Wasmer&lt;/code&gt; gained a feature that matters for real crypto workloads. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WasmEdge&lt;/code&gt; remained fast once the AOT run mode was explicit.&lt;/p&gt;

&lt;p&gt;The less comforting part is that WebAssembly performance still isn’t one thing. It depends on the runtime, the release, the enabled WebAssembly features, whether the code goes through WASI from JavaScript, and whether ahead-of-time native compilation is allowed.&lt;/p&gt;

&lt;p&gt;So benchmark your actual workload.&lt;/p&gt;

&lt;p&gt;But if your workload looks like libsodium, the answer in 2026 is: WebAssembly can be close to native, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wide_arithmetic&lt;/code&gt; is worth caring about, and yes, some runtimes really are getting faster.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Faster signatures for WebAssembly</title>
    <link href="https://00f.net/2026/06/17/ed25519-wasm-signatures/"/>
   <updated>2026-06-17T00:00:00+02:00</updated>
   <id>https://00f.net/2026/06/17/ed25519-wasm-signatures</id>
   <content type="html">&lt;p&gt;I just released &lt;a href=&quot;https://github.com/jedisct1/rust-ed25519-wasm&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;&lt;/a&gt;, a small Rust crate for Ed25519 signatures in WebAssembly. The static library can also be directly linked in applications written in other languages.&lt;/p&gt;

&lt;p&gt;It’s backed by &lt;a href=&quot;https://lib25519.cr.yp.to/&quot;&gt;lib25519&lt;/a&gt;, compiled to WebAssembly using Zig, linked to Rust bindings, and exposes a simple API similar to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-compact&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-rust highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;ed25519_wasm&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;KeyPair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;};&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key_pair&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;KeyPair&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;from_seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;Seed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;b&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key_pair&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.sk&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.sign&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;key_pair&lt;/span&gt;&lt;span class=&quot;py&quot;&gt;.pk&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.verify&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;.unwrap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-benchmark&quot;&gt;The benchmark&lt;/h2&gt;

&lt;p&gt;The benchmark in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt; README was run with Wasmtime 45.0.2, using 128-byte messages.&lt;/p&gt;

&lt;p&gt;The table reports the median time per operation and the relative speed compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Algorithm / implementation&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Sign 128B&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Verify 128B&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Sign vs ed25519-wasm&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;Verify vs ed25519-wasm&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Ed25519 / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;15.1 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;49.1 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.0x&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.0x&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ed25519 / libsodium wasm&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;21.4 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;53.1 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.4x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.1x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ECDSA-P256/SHA-256 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p256&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;245 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;277 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;16.2x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;5.6x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ML-DSA-44 / Zig std wasm&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;135.5 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;23.3 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8.9x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2.1x faster&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ML-DSA-44 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml-dsa&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;706 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;91.7 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;46.6x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;1.9x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RSA-2048 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsa&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.21 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;171 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;212x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3.5x slower&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;RSA-3072 / RustCrypto &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsa&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;10.2 ms&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;394 us&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;671x slower&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;8.0x slower&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ed25519&lt;/code&gt; beats everything hands down.&lt;/p&gt;

&lt;p&gt;If you already use libsodium, including from Rust through something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libsodium-rs&lt;/code&gt;, that’s a perfectly reasonable option.&lt;/p&gt;

&lt;p&gt;But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt; is slightly faster: 15.1 microseconds to sign and 49.1 microseconds to verify a 128-byte message on Apple M4.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P-256&lt;/code&gt; isn’t close.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ECDSA-P256/SHA-256&lt;/code&gt; with RustCrypto’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p256&lt;/code&gt; takes 245 microseconds to sign and 277 microseconds to verify. That’s 16.2 times slower for signing and 5.6 times slower for verification.&lt;/p&gt;

&lt;p&gt;RSA is much worse.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-2048&lt;/code&gt; signing takes 3.21 milliseconds. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-3072&lt;/code&gt; signing takes 10.2 milliseconds.&lt;/p&gt;

&lt;p&gt;Compared to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;, that’s &lt;strong&gt;212 times slower&lt;/strong&gt; and &lt;strong&gt;671 times slower&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Verification is less catastrophic, but still bad: 171 microseconds for RSA-2048 and 394 microseconds for RSA-3072.&lt;/p&gt;

&lt;h2 id=&quot;rs256-is-a-huge-waste-of-cpu-cycles-and-money&quot;&gt;RS256 is a huge waste of CPU cycles and money&lt;/h2&gt;

&lt;p&gt;Even when they are not forced to, people use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt;. Especially &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RS256&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt;’s jargon, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RS256&lt;/code&gt; means RSA with SHA-256. It’s common because old systems support it, cloud identity products support it, tutorials use it, and people copy what already works.&lt;/p&gt;

&lt;p&gt;But if you’re verifying or signing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt; tokens in WebAssembly, avoid &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RS256&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For signing, it isn’t even close. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-2048&lt;/code&gt; is already &lt;strong&gt;212 times slower&lt;/strong&gt; than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-3072&lt;/code&gt; is &lt;strong&gt;671 times slower&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Even verification, the operation &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JWT&lt;/code&gt; consumers usually care about, is &lt;strong&gt;3.5 times slower&lt;/strong&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-2048&lt;/code&gt; and &lt;strong&gt;8 times slower&lt;/strong&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA-3072&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If this runs in a browser once, maybe you don’t care.&lt;/p&gt;

&lt;p&gt;If this runs at the edge for every request, or in a WebAssembly function where CPU time is literally what you pay for, you should care a lot. It’s a terrible choice.&lt;/p&gt;

&lt;h2 id=&quot;what-about-post-quantum-signatures&quot;&gt;What about post-quantum signatures?&lt;/h2&gt;

&lt;p&gt;ML-DSA is interesting.&lt;/p&gt;

&lt;p&gt;It’s post-quantum resistant, and in WebAssembly, its performance is close to native since it’s mostly based on scalar arithmetic.&lt;/p&gt;

&lt;p&gt;The Zig standard library implementation of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ML-DSA-44&lt;/code&gt; compiled to WebAssembly signs in 135.5 microseconds and verifies in 23.3 microseconds in this benchmark.&lt;/p&gt;

&lt;p&gt;This is actually faster than Ed25519!&lt;/p&gt;

&lt;p&gt;But don’t generalize that to every ML-DSA implementation.&lt;/p&gt;

&lt;p&gt;RustCrypto’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ml-dsa&lt;/code&gt; implementation is much slower, and signs in 706 microseconds and verifies in 91.7 microseconds. That makes signing 46.6 times slower than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt;, and even verification is 1.9 times slower.&lt;/p&gt;

&lt;p&gt;But with a fast implementation, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ML-DSA&lt;/code&gt; is a wise choice for WebAssembly if you can afford the large keys and signatures.&lt;/p&gt;

&lt;h2 id=&quot;use-ed25519-or-ml-dsa-forget-rsa&quot;&gt;Use Ed25519 or ML-DSA, forget RSA&lt;/h2&gt;

&lt;p&gt;For most WebAssembly applications that need signatures, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ed25519&lt;/code&gt; should be your go-to algorithm.&lt;/p&gt;

&lt;p&gt;It’s got small keys, small signatures, simple APIs, fast signing, fast verification, and mature implementations.&lt;/p&gt;

&lt;p&gt;If you already ship libsodium to WebAssembly, keep using it. The WebAssembly build performs well, and libsodium remains a good default crypto toolbox.&lt;/p&gt;

&lt;p&gt;But if the only thing you need is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ed25519&lt;/code&gt; signatures from Rust targeting WebAssembly, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519-wasm&lt;/code&gt; is now the best option I know of.&lt;/p&gt;

&lt;p&gt;It’s small. It’s direct. It’s backed by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib25519&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you pay to run WebAssembly code, use this.&lt;/p&gt;
</content>
  </entry>
  
</feed>

