现充|junyu33

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 f(x)=0, and we already have an approximation xn. A Taylor expansion at (xn,f(xn)) gives the tangent line

y=f′(xn)(x−xn)+f(xn)

Setting y=0 gives xn+1=xn−f(xn)f′(xn). Repeat this, and we can approach an actual root of f(x)=0.

Of course, two important questions remain:

  1. Do we need to be picky about the initial guess x0?
  2. How fast does it converge?

For the first question, yes. A simple example: draw the tangent to f(x)=x2−2 at x=0, and it won't intersect the x-axis at all. The method fails immediately. A slightly less trivial example is f(x)=x3−2x+2, whose iteration is xn+1=xn−xn3−2xn+23xn2−2. Start at x0=0, and it just bounces between 0 and 1. Still no luck.

So here's some intuition: if we can pick an initial guess x0 that's reasonably close to the actual root, that's usually a pretty decent choice.

The answer to the second question is quadratic convergence. To get a feel for why, I'll skip the general proof and use f(x)=x2−2 as a concrete example.

We know its positive root is α=2, and the iteration is

xn+1=xn−xn2−22xn=xn2+22xn

First, let's show that it converges to 2 when xn>0. After the first iteration, AM–GM tells us that the result is at least 2. Then xn+1−xn=2−xn22xn≤0, so the sequence is decreasing and bounded below. It must converge. Call the limit L; substituting it into L=L2+22L gives L=2. Done.

Now let the error be en=xn−α, and try expressing the next error:

en+1=xn+1−α=xn2−2xnα+22xn=(xn−α)22xn=en22xn

This gives |en+1||en|2=12xn. We already know that the sequence converges to 2, so the right-hand side approaches a constant. That's our quadratic convergence.

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 ε as

ε=y−1/x1/x×100%.

For some familiar inputs x, we get:

Input x Integer i after the operation Resulting y 1/x Relative error
10−12 0x4970fb79 987063.6 1000000 −1.294%
10−6 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%
106 0x3a7d47df 0.000966189 0.001 −3.381%
1012 0x3582ef8d 9.75547×10−7 10−6 −2.445%
1030 0x2692607a 1.01569×10−15 10−15 +1.569%

The results aren't exactly accurate, but for a fairly well-behaved monotonic function like 1/x, they should be decent enough. So the guess checks out: this code does produce a reasonable initial approximation.


The next question, then, is why this bit-twiddling code can approximate 1/x at all. The idea isn't too hard to see. Consider the IEEE 754 representation of a 32-bit float: the top bit is the sign S, followed by the 8-bit exponent E and the 23-bit mantissa M. The mapping from this representation to the floating-point value is

(−1)S×2E−bias×(1.M)

Let's start with something simpler: x. If we ignore the bias for now, taking bias=0, shifting the IEEE 754 representation right by one bit gives a fairly good estimate. The sign stays the same, the exponent is roughly halved, and the mantissa becomes a mess. But since log⁡x=12log⁡x, the estimate makes sense.

In reality, though, bias=127, so a plain right shift obviously won't work. Try plugging in 1, and you'll be off by orders of magnitude. What we actually want to do with E is

E′−bias=12(E−bias)

Simplify this, and we get E'=(E+127)>>1. Now, 127>>1 is 63, while the floating-point value represented by 0x5f3759df is (1+36274878388608)263. We can start to see that this line of thought might get us somewhere with that mysterious line of code.


Now consider the expression we actually want. Similarly, log⁡1/x=−12log⁡x, so we have:

E′−bias=−12(E−bias)

In other words:

E′=32×127−12E

And ⌊3×1272⌋=190, which is exactly the exponent field of 0x5f3759df! The 12E part also lines up with 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 xn+1=xn−f(xn)f′(xn), but our code says 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 1/x, but plenty of equations have that root. Which one gives us this code? Maybe someone can just "notice" the answer. I couldn't, at least not this time. So I started by writing down the equation directly:

y−f(y)f′(y)=y(32−x2y2)

Rearranging gives

2f(y)=(xy3−y)f′(y)

Here f is a function of y. Great, we've got an ODE. Except I've long since forgotten how to solve ODEs, so I asked GPT and learned the following:

For a differential equation of the form

f′(y)=g(y)f(y)

on any interval where f≠0, divide both sides by f(y):

f′(y)f(y)=g(y)

Note that

ddyln⁡|f(y)|=f′(y)f(y)

So we can integrate directly:

ln⁡|f(y)|=∫g(y)dy+C

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 g(y)=2xy3−y, and we get:

ln⁡|f(y)|=∫2xy3−ydy+C

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:

∫2ax3−xdx=∫2ax3−xdx=∫(ux+vx+wax2−1)dx

Putting everything over a common denominator gives (au+v)x2+wx−u≡2. Solving for the coefficients gives u=−2,v=2a,w=0. Substitute them back and keep integrating:

∫(−2x+2axax2−1)dx=−2ln⁡|x|+ln⁡|ax2−1|+C=ln⁡|a−1x2|+C

Switching the notation back:

f(y)=C(x−1y2)

Check that 1/x is indeed a root of f, and there we have it: 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 x Approximation y True value 1/x 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 x is 1−22≈−29.289%. Proof omitted.

If we let the mantissa vary freely too, from 0x5f000000 to 0x5f7fffff, the global worst case occurs at 4/3. Floating-point precision rounds this to about 1.33333337, and the resulting maximum error εmax, relative to 1.33333337, is:

εmax=1.333333253−0.8660253910.866025391≈+53.960065%

That's tens of times the roughly 3% error we saw earlier. One Newton iteration isn't going to make up for all of that. This brings us to the algorithm's accuracy.

To pick the best magic number, we can try the following search strategy:

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:

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 f(a)>f(b)<f(c), a<b<c, the minimum must lie somewhere in (a,c). In this case, searching [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 e1<e2, Newton's method doesn't guarantee that the errors after an iteration still satisfy e1′<e2′. Why, exactly? That probably comes down to the properties of the function and how "decent" our initial guesses really are. It gets a bit complicated, so I'll skip it here.


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 224 normal inputs in [1,4) (why this interval?) and compared their minimax errors. The code is in Appendix C: Searching the interval [1,4). Our experimental candidates are the "best" values from the first two scripts, and the reference is Chris Lomont's Fast Inverse Square Root paper:

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):

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:

  1. What is Newton's method?
  2. What's the rough intuition behind the shift at the beginning, and how do we choose the exponent?
  3. Why is the last line really a Newton iteration?
  4. 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

E′−bias=−12(E−bias)

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 223 to 252. Even our earlier coarse-then-fine approach is pretty much impractical. But remember that V-shaped relationship we saw between the binary32 magic number and the error? That's a unimodal function. Besides the most naive exhaustive search, ternary search immediately came to mind. Why? Because it turned up in the OI part of my university entrance programming test five years ago. Ah, those were the days.

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 f with a minimum, start with an interval [a,b] and two trisection points m1=23a+13b and m2=13a+23b:

For this article, evaluating f still means calculating the minimax error on 10000 random normal binary64 values with the fixed seed 0xC0FFEE. I chose Δ=512 and evaluated 0 and 1 Newton iterations separately. The implementation is in Appendix D: Ternary search. Here are the preliminary results:

Objective Paper's result Our random-sample search result dist/252 εmax
0 Newton iterations 0x5FE6EC85E7DE30DA 0x5FE6EC853A893173 6.457×10−7 3.421%
1 Newton iteration 0x5FE6EB50C7B537A9 0x5FE6EB50BC36EFCB 4.282×10−8 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:


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 [1,4) is still wildly impractical, so I tried three schemes:

The implementation is in Appendix E. Here are the final results:

Newton iterations Method Magic number dist/252 εmax Δεmax (pp)
0 Paper 0x5fe6ec85e7de30da — 3.421281331784% —
Equally spaced 0x5fe6ec85e7c18a4c 4.169×10−10 3.421281298320% −3.346×10−8
Random 0x5fe6ec85e7b577a0 5.926×10−10 3.421281284218% −4.757×10−8
float32 grid 0x5fe6ec85e7a5ce2a 8.205×10−10 3.421281265925% −6.586×10−8
1 Paper 0x5fe6eb50c7b537a9 — 0.175118367122% —
Equally spaced 0x5fe6eb50c7b33619 2.919×10−11 0.175118366879% −2.429×10−10
Random 0x5fe6eb50c736bce6 1.841×10−9 0.175118351810% −1.531×10−8
float32 grid 0x5fe6eb50c77c4635 8.286×10−10 0.175118360228% −6.894×10−9

Here:

dist=|Mours−Mpaper|Δεmax=εmaxours−εmaxpaper.

Smaller Δεmax is better.

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:

The discrepancies after 1 Newton iteration aren't too hard to explain:

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 1.084×10−19. In some Windows environments, including this machine's Windows 11 setup, long double may have the same precision as double, which affects the last few digits.

Download appendix_a_coarse.py

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.

Download appendix_b_fine.py

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)

Download appendix_c_verify.py

Run:

python appendix_c_verify.py --chunk-size 262144

--chunk-size 262144 sets the number of inputs processed per batch. The script checks all 224 binary32 inputs in [1,4) and, by default, saves the CSV as fisr_full_range_results_python.csv in the current directory.

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

Download appendix_e_final.py

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 224 inputs; the output plots are appendix_e_cross_0_newton.png and appendix_e_cross_1_newton.png.