The fastest double-to-string algorithm you’ve never heard of<br>Posts<br>Talks<br>Papers<br>Projects- CATALOG -Where yy fits in<br>Four candidates<br>A step-by-step at E4M3 scale<br>A boundary case that looks like a bug<br>Try it<br>Fun fact
vitaut.net
The fastest double-to-string algorithm you’ve never heard of2026-08-10
Żmij, the binary-to-decimal conversion<br>library I wrote about a few posts back,<br>started as an optimized port of Schubfach. Later I switched its core to a different<br>algorithm, defined in yy_double.c<br>from yyjson by<br>ibireme. It has no paper, no name beyond<br>the file it lives in (I'll refer to it as yy), and almost no public<br>profile outside the JSON performance crowd. It also happens to be one of the<br>fastest<br>dtoa implementations.<br>This post is a tour of yy through a small visualization, with a<br>close look at one boundary case.<br>Where yy fits in<br>yy is in the<br>Schubfach<br>family. The shared idea, which I covered<br>in an earlier post, is to find<br>the shortest decimal $\sigma \cdot 10^{e_{10}}$ that round-trips back<br>to a binary float $v$ by intersecting $v$'s rounding interval with<br>decimal grids of various spacings, and picking the coarsest grid that<br>still has a tick in the interval.<br>yy's trick is doing this very cheaply. The whole algorithm runs on<br>fixed-width integer arithmetic and uses only one multiplication by a<br>precomputed power of 10, where classic Schubfach needs two or three.<br>Four candidates<br>For each binary float $v = c \cdot 2^{e_2}$, yy picks a decimal<br>exponent $e_{10}$ via a fixed-point approximation of $\log_{10} 2$,<br>then re-expresses $v$ at the decimal scale as<br>$$<br>\bar v \approx v \cdot 10^{-e_{10}}<br>$$<br>using a precomputed power-of-10 table $p_{10}$, a fixed-point value<br>with $p_{10} \cdot 2^{e_p} \approx 10^{-e_{10}}$ for some binary<br>exponent $e_p$. $\bar v$ then sits between four candidate decimal<br>values:<br>$d_1 = \lfloor \bar v \rfloor$ and $u_1 = d_1 + 1$, the integers<br>immediately below and above $\bar v$.<br>$d_0 = 10 \cdot \lfloor \bar v / 10 \rfloor$ and $u_0 = d_0 + 10$,<br>the multiples of 10 below and above.<br>Outputting $d_0$ or $u_0$ gives a decimal one digit shorter than<br>$d_1$ or $u_1$, because the trailing zero folds into the exponent.<br>Like classic Schubfach, yy prefers $d_0$ or $u_0$ when they round-trip<br>and falls back to $d_1$ or $u_1$ otherwise. Three predicates do the<br>work, evaluated against $\bar v$ and a half-ulp band $\delta$ around<br>it. The first checks<br>whether $\bar v - \delta$ reaches $d_0$:<br>$$<br>\delta \ge \bar v_{10} + \varepsilon_c<br>$$<br>with $\bar v_{10} = \bar v \bmod 10$. The second checks whether<br>$\bar v + \delta$ reaches $u_0$:<br>$$<br>\bar v_{10} + \delta \ge 10 + \eta_c<br>$$<br>The third decides between the longer candidates $d_1$ and $u_1$ by<br>checking whether $\bar v$ is past the midpoint:<br>$$<br>\bar v \bmod 1 \ge \tfrac12 + \varepsilon_u<br>$$<br>The biases $\varepsilon_c$, $\eta_c = 2\varepsilon_c - 1$,<br>$\varepsilon_u$ are small parity adjustments ($0$ or $\pm 1$) that<br>implement round-half-to-even at exact ties. The first predicate that<br>fires picks the candidate; if none does, the answer is $d_1$.<br>The $\varepsilon$ and $\eta$ terms are my bookkeeping, not yy's: they<br>let me write the three predicates as simple, uniform formulas. The code<br>doesn't adjust the thresholds at all. It runs the plain comparison and,<br>only when it lands exactly on a tie, branches off and rounds to even by<br>testing a low bit of the significand.<br>This is where the one-multiplication claim from earlier comes in:<br>$\delta$ doesn't need its own multiplication. The half-ulp of $v$ is<br>$\tfrac12 \cdot \mathrm{ulp}(v) = 2^{e_2 - 1}$, which in $\bar v$'s scale<br>gives<br>$$<br>\delta = 2^{e_2 - 1} \cdot p_{10} \cdot 2^{e_p}<br>= p_{10} \cdot 2^{e_2 + e_p - 1}<br>$$<br>so $\delta$ is just $p_{10}$ shifted by an integer (no rounding, no<br>second multiplication). The bounds of the rounding interval are then<br>$\bar v \pm \delta$ via add and subtract. Schubfach instead multiplies<br>$v$, $v_l$, and $v_r$ by $p_{10}$ separately, which is two extra<br>192-bit multiplications.<br>The actual algorithm is a bit more involved than this, with extra<br>paths for irregular intervals, subnormals, and the digit-emission<br>loop. The sketch above is the core idea the rest hangs off of, and<br>all you need to follow the visualization.<br>A step-by-step at E4M3 scale<br>E4M3 is an 8-bit floating-point format (1 sign bit, 4 exponent bits,<br>3 significand bits, bias 7) used for low-precision AI inference on<br>recent GPUs. With only 256 encodings it fits on one page, which<br>makes it a good target for visualizing things you'd otherwise have<br>to take on faith at f64 scale. I went into more detail on the format<br>in the previous post.<br>The walk-through is one HTML page,<br>e4m3-yy.html; open it in a new<br>tab.<br>The page walks a value through yy's pipeline top to bottom. The main<br>grid at the top plots every E4M3 value, with the rounding interval of<br>the selected value highlighted:
The middle panel is yy itself, step by step: $e_{10}$, the $p_{10}$<br>table with the active row highlighted, the scaling chain<br>$\bar v = c \cdot 2^{e_2} \cdot p_{10}...