
Bringing correctly rounded math to production
As we mentioned in the last cmath blog post, MSVC is using LLVM-libc for compile-time evaluation and runtime execution of math functions when /Zc:cmath is enabled. I invited LLVM-libc contributors Michael Jones and Tue …
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
As we mentioned in the last cmath blog post, MSVC is using LLVM-libc for compile-time evaluation and runtime execution of math functions when /Zc:cmath is enabled. I invited LLVM-libc contributors Michael Jones and Tue Ly to write a guest blog post about how they’ve achieved what they’ve achieved. This is that blog post! I hope you enjoy it. I learned a lot from reading it, even after having spent a lot of time in this area.
– Cody, Microsoft C++ Compiler Frontend Engineer
Hi, I’m Michael Jones, the lead maintainer for LLVM-libc. Cody invited me and our math lead, Tue Ly, to write a guest post about LLVM-libc’s math functions. Tue and I work on LLVM-libc at Google, where we’ve been working on the project for about five years now.
I’m going to talk a bit about the history and philosophy of LLVM-libc, then Tue will do a deeper dive into how and why our math functions are so good. I’m really excited for you all to use the code we’ve been working on, and thanks to Cody for putting all of this together!
Generally, implementations of the C standard library (libc) are written for a specific target. They are usually monolithic libraries that are closely tied to the OS interface they’re based on. They also don’t make any effort to keep individual functions separate, which makes them difficult to break into smaller chunks.
In 2019, LLVM-libc was started with a different design philosophy. The focus was on portability, modularity, and writing clean C++ that could be reused. The original design describes this as writing “libc as a library”, meaning that it would be much closer to an idiomatic C/C++ library. From the beginning, there has been a separation between the OS interface layer and the public interface, as well as making each individual function independent. Portability has also involved avoiding assembly except where absolutely necessary, which has the added benefit of enabling the compiler to make deeper optimizations.
Building on the modular design, I started Project Hand-in-Hand which allowed LLVM’s libc++ to use the same float-to-string conversion code as LLVM-libc. Other LLVM projects have also started to integrate LLVM-libc’s code, such as OpenMP using the printf core and compiler-rt using the software floating point support. Soon both clang and MSVC will be using LLVM-libc’s math library for constexpr evaluation, but what makes LLVM-libc’s math suitable for compiler use?
I feel like I know more than the average programmer about floating point numbers, but I would not consider myself an expert. Tue Ly is definitely an expert, with the experience and credentials to prove it. He was the one who designed LLVM-libc’s math library from the ground up, and it was his idea to make it correctly rounded for all rounding modes. This turned out to be hugely important to Google for much the same reason it’s important to Microsoft: different versions of your math library returning different results can have disastrous consequences when working with heterogeneous distributed systems. I’ll let Tue go into the details of exactly how the math functions are still performant while being so accurate, as well as the path to get there.
Thanks, Michael! And thanks to Cody for inviting us to contribute to this series.
In the previous blog post, Cody laid out the four dimensions users care about when it comes to math libraries: Stability, Accuracy, Consistency, and Performance (or, in Cody’s words, “Numbers go brrr”).
Floating-point math in software is often treated like dark magic. When developers see different results across platforms or compilers, they often shrug and say, “Well, floating-point is inexact.” But inexact representation does not mean mathematical operations have to be non-deterministic.
Traditional math libraries (libm) frequently produce different results across platforms and versions. In production systems, these differences cause real issues:
Developers have dealt with floating-point differences across platforms for a long time, as seen across game development, physics simulations, and language standardization:
However, all of these traditional workarounds share an inherent limitation: while compiler flags and hardware normalization can achieve reproducibility for basic arithmetic operations (+, −, ×, /, √), they fall short for transcendental functions (sin, cos, exp, log, etc.), which form a substantial part of standard math.h.
Basic arithmetic operations are strictly specified and hardware-mandated to be correctly rounded under IEEE-754. But transcendental functions in standard math.h are almost always software approximations where IEEE-754 has historically not mandated bit-level exactness. Consequently, their output bits are simply an artifact of a specific library’s polynomial approximations, range-reduction splits, or lookup table values.
The moment a library maintainer updates a function to make it faster, reduce binary size, or vectorize it, the least significant bits will inevitably shift for many inputs.
Freezing compiler flags or using custom lookup tables cannot protect you from future library updates. You are left choosing between never updating dependencies or frequently updating downstream tests.
This is where correct rounding fixes the problem.
Under IEEE-754, if a math function is guaranteed to be correctly rounded across all standard rounding modes (round-to-nearest, round-up, round-down, and round-to-zero), the output bit pattern for any given input is mathematically unique, meaning there is only one correct answer.
By solving for Accuracy through correct rounding, you automatically get Stability and Consistency for free. Because the output is mathematically determined rather than an implementation detail, library authors can completely rewrite, optimize, and vectorize their algorithms across future releases without shifting output bits. Your math functions return the exact same bits on x86-64, ARM64, Windows, Linux, across datacenters, across library updates, and, with C++23 (P0533R9) and C++26 (P1383R2) adding constexpr math, between compile-time evaluation and runtime execution.
In his blog post, Cody joked that writing a complete, accurate math library seemed to require “years of monk-like study.” Cody wasn’t wrong, except it was decades of research across the computer arithmetic community.
For decades, correct rounding for transcendental functions (eˣ, log x, sin x, etc.) was considered too slow for production systems due to the Table-Maker’s Dilemma.
The dilemma is simple to state but hard to solve: when computing an approximation ŷ of a transcendental function y = f(x), how many extra bits of intermediate precision do you need to guarantee that rounding ŷ produces the exact same floating-point value as rounding the exact mathematical value f(x) ?
If the true mathematical result falls very close to a rounding midpoint (for round-to-nearest) or a representable floating-point boundary (for directed rounding), an approximation with standard precision cannot tell whether to round up or down.
The first major attempt to bring correctly rounded transcendentals to production was Abraham Ziv’s work on IBM libultim in the 1990s (later used in glibc). Ziv introduced the multi-stage evaluation strategy: compute a fast approximation; if the result is too close to a rounding boundary (Ziv’s rounding test), fall back to a higher-precision path.
However, because the maximum required precision was unknown at the time, libultim‘s fallback path used arbitrary-precision arithmetic scaling up to 768 bits of precision (via internal helpers like slowpow.c and mppow.c). When programs hit one of these hard-to-round inputs, performance dropped sharply: while a normal fast-path pow took ~70 cycles, the 768-bit fallback took up to 440,000 cycles (as documented in glibc Bug 13932 and Bug 16898) which is a ~6,000× slowdown on a single function call!
These unpredictable slowdowns caused performance issues in production workloads, eventually leading glibc maintainers to remove libultim in favor of faster, non-correctly rounded routines with bounded latency. As a result, correct rounding got a reputation for being “too slow for production.”
To avoid these slowdowns, the arithmetic research community needed to answer a fundamental question: What is the worst-case hard-to-round input across the entire floating-point domain? If you know the worst case, you know the maximum precision ever required, and you can build a bounded, non-allocating fallback path.
Finding these worst cases is very difficult. In double precision, searching the 2⁶⁴ input space by brute force was long considered computationally prohibitive.
The breakthrough began with the foundational work of Vincent Lefèvre and Jean-Michel Muller (Arénaire / AriC at ENS Lyon / CNRS / Inria). In pioneering publications like Toward correctly rounded transcendentals (Lefèvre, Muller, & Tisserand, 1998) and Worst Cases for Correct Rounding of the Elementary Functions in Double Precision (Lefèvre & Muller, 2001), they proved that the worst-case precision required for double-precision functions could be systematically determined. Their work culminated in the release of CR-LIBM, demonstrating for the first time that double-precision elementary functions could be correctly rounded with reasonably bounded intermediate precision.
Over the last several years, this search was completed. Through our close collaboration with Paul Zimmermann and Vincent Lefèvre, culminating in our recent paper Computing hard-to-round cases of sin, cos, tan in double precision (ARITH 2026), we have now systematically searched and proven the worst-case bounds for all standard univariate double-precision math functions.
Most importantly, all of these worst-case inputs and mathematical bounds are openly hosted and maintained in the CORE-MATH project repository (and documented at core-math.gitlabpages.inria.fr), serving as an open, shared ground truth for library implementers worldwide.
Around 2020, when we started architecting the math library for LLVM-libc, several independent developments were converging:
The realization that the timing was finally right wasn’t unique to LLVM-libc. Around the exact same time, two other major research efforts started with the same objective:
From the very beginning, the LLVM-libc math team established an active collaboration with CORE-MATH and RLIBM: discussing algorithm approaches, sharing hard-to-round test vectors, developing parallel implementations, and cross-validating each other’s implementations.
This collaboration helped us verify correctness, optimize performance, and prove that correctly rounded math is fast enough for production use.
When developers hear that LLVM-libc guarantees correct rounding, their immediate reaction is often skepticism: “If you’re checking every last bit against the exact mathematical value, how is your library not slow?”
The answer lies in how we structure our execution pipeline around modern hardware strengths:
When an input comes in, we don’t immediately jump into high-precision math. Instead, our first stage evaluates a fast, carefully tuned Taylor or minimax polynomial approximation using double-double or truncated double-double arithmetic to achieve 70-106 bits of precision, heavily accelerated by native hardware FMA instructions. We break the function’s domain into small intervals such that lookup tables (if needed) comfortably reside in the CPU’s L1 cache.
Once the fast path computes this higher-than-double approximation, we perform Ziv’s rounding test: we check whether the error interval contains a rounding boundary. If the answer is clear (which happens for over 99.99% of all possible inputs), we round to the target precision and return immediately. In terms of latency and throughput, this fast path is very similar to fast, non-correctly rounded math libraries.
What about that rare fraction of a percent (<0.01%) of tricky, hard-to-round inputs whe
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。