To determine what we can improve upton the current implemetation in dtoa.c I implemented float-to-string and string-to-float conversions with modern libraries. For string-to-float, the options I looked at are ffc.h (a C99 port of Lemire’s fast_float) and Wuffs. Both can drop into the string-to-float path currently provided by _Py_dg_strtod in dtoa.c. There is no significant performance gain (or loss), but both packages provide a modern and well-tested routine.
For float-to-string the situation is a bit more complex. _Py_dg_dtoa has three different modes:
| dtoa mode |
Used by |
Description |
| Mode 0 |
repr(), str() |
Shortest round-trip representation |
| Mode 2 |
%e, %g, f-string e/g |
N significant digits |
| Mode 3 (ndigits ≥ 0) |
%f, f-string f |
N digits after the decimal point |
| Mode 3 (ndigits < 0) |
float.__round__ with negative ndigits |
Rounding to a power of 10 |
Of the C libraries available, Ryu (used by Rust, Swift, Julia, and Zig) provides most of the required functionality. Using Ryu for Mode 0 and Mode 3 with ndigits ≥ 0 gives a clear performance improvement:
| Benchmark |
main ns/op |
Ryu ns/op |
Speedup |
| Mode 0 — shortest repr |
|
|
|
repr(x), fixed set |
178.1 |
102.6 |
1.74× |
repr(x), 100 random floats |
410.8 |
101.3 |
4.06× |
| Mode 3 — fixed-point (positive ndigits) |
|
|
|
f'{x:.2f}', fixed set |
144.8 |
151.2 |
0.96× |
f'{x:.10f}', fixed set |
168.5 |
168.1 |
1.00× |
f'{x:.10f}', 100 random floats |
339.1 |
176.2 |
1.92× |
(Release build, gcc 14.2 on Linux x86_64, best ns/op over 7 samples.)
The fixed set is 0.0, 1.0, -1.0, 0.1, 0.5, 1.5, 3.14, 2.718281828, 1/7, 1/3, 12345.6789, -98.76, 9.99, 100.0, 0.001, 1e-5, -2.5, 42.42, 0.25, 987.654. Note that for the fixed set (“easy” values where Gay’s dtoa hits a fast path) performance is very different from the random floats.
A branch implementing the above is at eendebakpt:ryu_float_partial. When disregarding the vendored Ryu and added tests, the changes to cpython itself are small (last two commits).
In an ideal scenario a replacement for dtoa.c would give us all
three of:
- Significant performance improvement
- A modern, well-tested library with minimal interface to cpython
- Full replacement of
dtoa.c
Achieving all three together is not easy. The string-to-float side is relatively easy. The float-to-string side has challenges: many of the available libraries are C++ (Dragonbox, Google double-conversion, others). The only C option with competitive performance I am aware of is Ryu.
- Mode 0 and Mode 3 with ndigits ≥ 0: Ryu works well out of the box.
- Mode 2 (
%e / %g): Ryu’s d2exp has a slight regression versus Gay’s dtoa. With some additional changes on ryu this can be fixed.
Mode 3 with ndigits < 0: is not implemented in Ryu. I have a prototype (based on ideas from Gay’s algorithm). It works, but it means diverging from the Ryu implementation.
That leaves us with roughly two options:
i) Use Ryu only for mode 0 and mode 3 with ndigits ≥ 0. Solid performance gain, but keeps dtoa.c around as the fallback for unsupported modes.
ii) Use Ryu for all modes. Larger performance gain and lets us delete dtoa.c, but requires more work on the Ryu adapter (mode-2 fast path, mode-3-negative extension).
Any feedback is welcome. If you are interested in working together on a potential PR, send me a DM.