Why does fast inverse square root work?
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
So what the fuck?
I'll assume you're already familiar with numerical analysis, IEEE 754 floating-point representation, and basic C.
Newton's method
The comments mention "iteration" twice, so Newton's method naturally comes to mind. Here's how it works.
Suppose we want to approximately solve
Setting
Of course, two important questions remain:
- Do we need to be picky about the initial guess
? - How fast does it converge?
For the first question, yes. A simple example: draw the tangent to
So here's some intuition: if we can pick an initial guess
The answer to the second question is quadratic convergence. To get a feel for why, I'll skip the general proof and use
We know its positive root is
First, let's show that it converges to
Now let the error be
This gives
Approximating the initial guess
Back to the five lines before the first iteration. Given the limitations of Newton's method we just discussed, it's reasonable to guess that i = 0x5f3759df - ( i >> 1 ); produces a rough but decent initial approximation. We can write a little script to check a few values first.
Define the relative error
For some familiar inputs
| Input |
Integer |
Resulting |
Relative error | |
|---|---|---|---|---|
0x4970fb79 |
987063.6 | 1000000 | −1.294% | |
0x44743e01 |
976.9688 | 1000 | −2.303% | |
| 0.001 | 0x41f5d0a8 |
30.72688 | 31.62278 | −2.833% |
| 0.01 | 0x41256e5a |
10.33944 | 10 | +3.394% |
| 0.1 | 0x4050f379 |
3.264860 | 3.162278 | +3.244% |
| 0.5 | 0x3fb759df |
1.432430 | 1.414214 | +1.288% |
| 1 | 0x3f7759df |
0.966215 | 1 | −3.378% |
| 2 | 0x3f3759df |
0.716215 | 0.707107 | +1.288% |
| 3 | 0x3f1759df |
0.591215 | 0.577350 | +2.401% |
| 4 | 0x3ef759df |
0.483108 | 0.5 | −3.378% |
| 10 | 0x3ea759df |
0.326858 | 0.316228 | +3.361% |
| 100 | 0x3dd359df |
0.103199 | 0.1 | +3.199% |
| 1000 | 0x3cfa59df |
0.030560 | 0.031623 | −3.359% |
0x3a7d47df |
0.000966189 | 0.001 | −3.381% | |
0x3582ef8d |
−2.445% | |||
0x2692607a |
+1.569% |
The results aren't exactly accurate, but for a fairly well-behaved monotonic function like
The next question, then, is why this bit-twiddling code can approximate
Let's start with something simpler:
In reality, though,
Simplify this, and we get E'=(E+127)>>1. Now, 127>>1 is 63, while the floating-point value represented by 0x5f3759df is
Now consider the expression we actually want. Similarly,
In other words:
And 0x5f3759df! The - ( i >> 1 ) that follows. Shifting the entire IEEE 754 representation right by one really does halve the exponent. If it's odd, we lose another 0.5, but that bit actually ends up in the top bit of the mantissa, so it's not a huge problem.
So we can conclude that the really important part of the magic number 0x5f3759df is its exponent. The mantissa affects the accuracy of the initial guess, and therefore the final accuracy after Newton's method, but doesn't make much difference to the correctness of the algorithm itself.
We'll come back to how the mantissa affects accuracy in Can we find a better mantissa?, two sections down.
Back to Newton's method
Now for the most headache-inducing part of this whole article. We know that Newton's iteration is y = y * ( threehalfs - ( x2 * y * y ) );. It looks vaguely similar, but I can't just match them up at a glance.
The problem is that we know the root is
Rearranging gives
Here
For a differential equation of the form
on any interval where
, divide both sides by : Note that
So we can integrate directly:
This is the basic method for solving a first-order separable differential equation.
Your equation is exactly this kind. You don't need higher-order ODEs, systems of ODEs, or numerical ODE methods.
Lesson learned. Substitute
For integrals of rational functions, I vaguely remember that we're supposed to split the fraction up. Let's first switch to some more familiar notation:
Putting everything over a common denominator gives
Switching the notation back:
Check that y = y * ( threehalfs - ( x2 * y * y ) ); really is a standard Newton iteration.
Can we find a better mantissa?
Assume all our floating-point inputs are normal. Let's return to the mantissa we ignored earlier. If we throw away that whole mess and change the code to i = 0x5f000000 - (i >> 1);, here are the errors for a few fairly extreme examples:
| Input |
Approximation |
True value |
Relative error |
|---|---|---|---|
| 0.5 | 1 | 1.414214 | −29.289% |
| 1 | 0.75 | 1 | −25.000% |
| 2 | 0.5 | 0.707107 | −29.289% |
| 3 | 0.4375 | 0.577350 | −24.223% |
| 4 | 0.375 | 0.5 | −25.000% |
We can also show that, with the magic number fixed at 0x5f000000, the worst relative error over inputs
If we let the mantissa vary freely too, from 0x5f000000 to 0x5f7fffff, the global worst case occurs at 1.33333337, and the resulting maximum error 1.33333337, is:
That's tens of times the roughly
To pick the best magic number, we can try the following search strategy:
- My intuition says that tiny changes in the mantissa
shouldn't make the error fluctuate wildly. So we can start by sampling magic numbers at a step size of 0x1000, from 0x5f000000to0x5f7ff000. - For the input
, randomly sample 10000 positive binary32 values and calculate the errors. Then, when searching within a 0x1000-wide interval, draw another 10000 positive binary32 values and keep them fixed. Evaluate all 4096 magic numbers on that same set and pick the few with the lowest errors.
Which brings up an important question: what exactly do we mean by "best", or "lowest error"? Having written papers before, I immediately came up with a few metrics:
- Minimize the geometric mean: a measure of overall error, though zero errors need to be replaced with a tiny
. - Minimize the average: account for the effect of outliers on the data.
- Minimize the minimax error, the worst error: extreme outliers carry the most weight.
I chose the third metric, because this code comes from the FPS game Quake III Arena. Speaking as a gamer, I'd rather things be slightly worse on average than suddenly freeze up. So let's optimize the worst case as much as we can.
With an objective chosen, we can start the coarse search and see how the minimax error varies with the magic number. As I guessed earlier, it shouldn't be too turbulent. For reproducibility, the random seed is 0xC0FFEE. Here's the plot; the full experiment code is in Appendix A: Coarse minimax search.
My intuition checks out: the curves do form a V shape. With 0 Newton iterations in blue and 1 iteration in yellow, both reach their minima at 0x5f376000, with errors of 3.427736% and 0.176516%, respectively.
Next we need a range for the fine search. For a V-shaped function, the intuition is that if [0x5f375000, 0x5f377fff] should be enough. See Appendix B: Fine minimax search for the code.
Then something interesting happens: the best constant for 0 iterations isn't the best constant for 1 iteration! The former is 0x5f37642e; for the latter, 0x5f375a71 and 0x5f375a73 tie for first place. Here are the two curves:
This means something rather curious: given two initial errors
Of course, there's another question: why are both results close to the original magic number 0x5f3759df, but not exactly the same? The reason is fairly mundane. Our sample isn't large enough, so the selected set inevitably has some statistical bias. Think of tossing a coin 1000 times: you're unlikely to get exactly 500 heads and 500 tails. It'll lean one way by a handful of tosses, or a dozen or so. The fine search then finds an optimum specific to that sample.
The fix is equally brute-force: cover all the distinct relative-error patterns. I chose all
| Magic number | 0 Newton iterations | 1 Newton iteration |
|---|---|---|
Quake III 0x5f3759df |
3.437577282% | 0.175233867% |
0x5f37642e (optimized for 0 iterations) |
3.421289021% | 0.177588172% |
Lomont 0x5f37642f (same objective) |
3.421283763% | 0.177588946% |
0x5f375a87 (optimized for 1 iteration) |
3.436540281% | 0.175128778% |
Lomont 0x5f375a86 (same objective) |
3.436546454% | 0.175130156% |
A few observations (pp = percentage point):
- The constants Lomont and I chose for 0 Newton iterations reduce the error by about 0.016 pp compared with the Quake III
0x5f3759dfbaseline, but their errors after 1 iteration are about 0.002 pp worse. - The constants Lomont and I chose for 1 Newton iteration actually improve on the baseline both before and after the iteration, by about 0.001 pp and 0.0001 pp, respectively.
- Comparing my results with Lomont's, his 0-iteration constant
0x5f37642fis slightly better than my0x5f37642e; for 1 iteration, my0x5f375a87is slightly better than0x5f375a86. In both cases, it's just a 1-bit difference! Given that floating-point rounding itself introduces errors on the order of, I'd say these "optimal" constants are pretty much neck and neck.
So, back to this section's title: at least under our imagined Quake III optimization philosophy, yes.
What about 64 bits?
Let's recap what we explored for 32 bits:
- What is Newton's method?
- What's the rough intuition behind the shift at the beginning, and how do we choose the exponent?
- Why is the last line really a Newton iteration?
- How do we search the mantissa for the best minimax result?
The Newton iteration itself doesn't change when we move from binary32 to binary64, so there's no need to revisit 1 and 3. We only need to look at 2 and 4.
For question 2, the answer is immediate. The equation
still holds; the bias just changes from 127 to 1023. Work it out, and the exponent field becomes 1534.
For question 4, the magic-number search space has grown from
In the memoir I wrote back then:
"Back to problem 2. I gave up on the mathematical approach and tried ternary search. Still got zero because of precision issues. In a fit of rage, I changed every variable to double. Things finally started moving, and I achieved the excellent score of 8 points."
That problem was worth 10 points. Back then, I used double to improve my ternary search. Now I'm using ternary search to improve the FISR magic number for double. Kind of beautiful, really.
Anyway, ternary search itself is simple. For a unimodal function
- If
, update . - If
, update . - For robustness, once
is smaller than some chosen distance , switch back to exhaustive search.
For this article, evaluating 0xC0FFEE. I chose
| Objective | Paper's result | Our random-sample search result | ||
|---|---|---|---|---|
| 0 Newton iterations | 0x5FE6EC85E7DE30DA |
0x5FE6EC853A893173 |
3.421% | |
| 1 Newton iteration | 0x5FE6EB50C7B537A9 |
0x5FE6EB50BC36EFCB |
0.175% |
The reference here is Improving the Accuracy of the Fast Inverse Square Root by Modifying Newton–Raphson Corrections. Like Lomont's paper, it derives the minima through detailed inequalities. A couple of observations:
- Our magic numbers aren't exactly the same as the paper's, but the differences amount to less than 1 bit at binary32 scale.
- The differences in
between the two choices are also within the order of .
Of course, 10000 random normal binary64 values still aren't enough if we want to avoid the selection bias mentioned earlier. We need a larger sample. Exhaustively checking all normal floating-point values in
equally spaced inputs in . random inputs in . - The same binary32 grid in
we tested earlier, converted directly to binary64.
The implementation is in Appendix E. Here are the final results:
| Newton iterations | Method | Magic number | |||
|---|---|---|---|---|---|
| 0 | Paper | 0x5fe6ec85e7de30da |
— | 3.421281331784% | — |
| Equally spaced | 0x5fe6ec85e7c18a4c |
3.421281298320% | |||
| Random | 0x5fe6ec85e7b577a0 |
3.421281284218% | |||
| float32 grid | 0x5fe6ec85e7a5ce2a |
3.421281265925% | |||
| 1 | Paper | 0x5fe6eb50c7b537a9 |
— | 0.175118367122% | — |
| Equally spaced | 0x5fe6eb50c7b33619 |
0.175118366879% | |||
| Random | 0x5fe6eb50c736bce6 |
0.175118351810% | |||
| float32 grid | 0x5fe6eb50c77c4635 |
0.175118360228% |
Here:
Smaller
To check whether these schemes avoid overfitting, I also did some cross-evaluation: take the winner from each sampling scheme, test it on the other two input sets, and compare everything with the paper's baseline. The plots look like this:
So our final conclusions are:
- With 0 Newton iterations, our selected constants consistently outperform the paper's magic number.
- With 1 Newton iteration, at least across these equally spaced, random, and binary32-grid cases, the magic number selected from the equally spaced samples consistently outperforms the paper's magic number.
The discrepancies after 1 Newton iteration aren't too hard to explain:
- Equally spaced inputs are like that perfectly balanced coin-toss case with 500 heads and 500 tails. No sampling imbalance.
- Random sampling is the least uniform, making it prone to overfitting; it also degrades noticeably on the equally spaced test set.
- The binary32 grid isn't evenly spaced because of the IEEE 754 representation itself, but it's still much better than the bias in the random sample. So it lands somewhere in between.
Reference code
You'll need NumPy and Matplotlib:
python3 -m pip install numpy matplotlib
I'd recommend running this on Linux, where the precision can reach long double may have the same precision as double, which affects the last few digits.
Appendix A: Coarse minimax search
Run:
python appendix_a_coarse.py
No command-line arguments. The script uses 10,000 random normal binary32 inputs, seed 0xC0FFEE, and step size 0x1000. It saves appendix_a_coarse.png next to the script.
Appendix B: Fine minimax search
Run:
python appendix_b_fine.py --output-dir .
--output-dir . saves the CSV and plots in the current directory, including fisr_minimax_fine_0_newton.png, fisr_minimax_fine_1_newton.png, and fisr_minimax_fine_comparison.png.
Appendix C: Searching the interval [1,4)
Run:
python appendix_c_verify.py --chunk-size 262144
--chunk-size 262144 sets the number of inputs processed per batch. The script checks all fisr_full_range_results_python.csv in the current directory.
Appendix D: Ternary search
Download appendix_d_ternary.py
Run:
python appendix_d_ternary.py --samples 10000 --seed 0xC0FFEE --bracket-width 512
--samples 10000 sets the random sample size, --seed 0xC0FFEE fixes the random seed, and --bracket-width 512 sets the width of the interval searched exhaustively after ternary search stops. Results are saved to fisr_binary64_ternary_results.csv by default.
Appendix E: Final binary64 experiment
Run:
python appendix_e_final.py --stage all --out-dir .
--stage all runs the optimization, cross-evaluation, and plotting for all three sampling schemes. --out-dir . saves the CSV files and plots in the current directory. Each dataset contains appendix_e_cross_0_newton.png and appendix_e_cross_1_newton.png.