Everything I know about the fast inverse square root algorithm
github.com
github.com
That being said, the techniques discussed here are not totally irrelevant (yet). There still exists some hardware with fast instructions for float/int conversion, but lacking rsqrt, sqrt, pow, log instructions, which can all be approximated with this nice trick.
Same as Carmack's, we did a single step of Newton's method and it was definitely good enough.
https://web.archive.org/web/20120124193536id_/http://www.acs...
Both _mm_rcp_ps (rcpps) and _mm_rsqrt_ps (rsqrtps) are only good for about half the bits.
gpu instruction sets, arm, risc-v, avr, pic, 8051, fpga... of course, in many cases, these do have a built-in approximate reciprocal square root operation, but probably implemented with this algorithm
gpu instruction sets, arm, risc-v, avr, pic, 8051, fpga... of course, in many cases, these do have a built-in approximate reciprocal square root operation, but probably implemented with this algorithm
> It's important to note that this algorithm is very much of its time. Back when Quake 3 was released in 1999, computing an inverse square root was a slow, expensive process. The game had to compute hundreds or thousands of them per second in order to solve lighting equations, and other 3D vector calculations that rely on normalization. These days, on modern hardware, not only would a calculation like this not take place on the CPU, even if it did, it would be fast due to much more advanced dedicated floating point hardware.
Calculations like this definitely take place on the CPU all the time. It's a common misconception that games and other FLOP-heavy apps want to offload all floating-point operations to the GPU. In fact, it really only makes sense to offload large uniform workloads to the GPU. If you're doing one-off vector normalization--say, as part of the rotation matrix construction needed to make one object face another--then you're going to want to stay on the CPU, because the CPU is faster at that. In fact, the CPU would remain faster at single floating point operations even if you didn't take the GPU transfer time into account--the GPU typically runs at a slower clock rate and relies on parallelism to achieve its high FLOP count.
In the old days, FPU do async computation.
FPU is now a considered an integrated part of CPU.
% Constants
FISRCON GREG #5FE6EB50C7B537A9
THREHAF GREG #3FF8000000000000
% Save half of the original number
OR $2,$0,0
INCH $2,#FFF0
% Bit level hacking
SRU $1,$0,1
SUBU $0,FISRCON,$1
% First iteration
FMUL $1,$2,$0
FMUL $1,$1,$0
FSUB $1,THREHAF,$1
FMUL $0,$0,$1
% Second iteration
FMUL $1,$2,$0
FMUL $1,$1,$0
FSUB $1,THREHAF,$1
FMUL $0,$0,$1
This implementation makes an assumption that the original number is greater than 2^-1021.https://github.com/ncruces/fastmath/blob/main/fast.go
Also see this StackOverflow:
https://stackoverflow.com/questions/32042673/optimized-low-a...
I just wrote this years ago (go.mod said 1.12) for the fun of it, thought I had it in a Gist/GitHub, and uploaded it yesterday in response to this post.
One thing I remember trying was coding this up in ASM… which makes it worse, because it prevents inlining. But I learned the Go ASM syntax that way.
The formulas for the floats have typos. They should read (-1)^S, not -1^S (which always equals -1).
Interpreting the raw bit patterns isn't a piecewise linear approximation of the logarithm. The lines between the data points on the blue graph don't actually exist, it's not possible for a bit to be half set to 1. It's rather a discrete version of the logarithm: the only data points that exist - where the red and blue lines meet - are literally equal to the (scaled, shifted) logarithm.
Other than that, nice post!
But! These numbers' base two logarithms imply mantissae of
- .0000000 (equal to the float 000)
- .0010101 (not equal to the float 001)
- .0101001 (not equal to the float 010)
- .0111010 (not equal to the float 011)
- .1001010 (not equal to the float 100)
- .1011001 (not equal to the float 101)
- .1100111 (not equal to the float 110)
- .1110100 (not equal to the float 111)
because the floats in the [2,4) interval are linearly spaced, whereas the corresponding logarithms are not. In other words, the floats are a piecewise linear approximation of the logarithm – just as the article says.
In any case, my point was more that it's not "piecewise linear". Piecewise linear means that the map is defined on some interval; it's not, it's defined on a discrete set of values. To take your example, the map isn't defined on e.g., 2.3. You can decide to interpolate linearly between the value at 2.25 and the value at 2.5, but that's a decision you make which isn't reflected in the code.
Or said differently: do you consider that any map defined on a discrete subset of R is really a piecewise linear map?
I mean it's not continuous, but it's still on an interval scale.
Given that the article is talking about operations on 32 bit integers and IEEE-754 32-bit floats, I personally think it's fine to leave out "discrete" in the description.
> The exact steps to go from the first form to this one are numerous to say the least, but I've included it in full for completeness.
The algebra included after that line has many unnecessary steps, as well as multiple cancelling sign errors. Most notably the second line to the third line does not distribute the negative sign correctly. Picking up after the second line, I would simply have:
y_n+1 = y_n + (1 - x * y_n^2) / y_n^2 * (y_n^3 / 2)
y_n+1 = y_n + (1 - x * y_n^2) * (y_n / 2)
y_n+1 = y_n + y_n / 2 - x * y_n^3/2
y_n+1 = 3/2 * y_n - x * y_n^3/2
y_n+1 = y_n (3/2 * y_n - x * y_n^2 / 2)
y_n+1 = y_n (1.5 * y_n - 0.5 * x * y_n * y_n)
This is fewer than 1/3 of the included steps (between the second line and the end), and has the advantage of being correct during the intermediate steps. I don't think any of my steps are anything other than self-evident to someone who understands algebra, but I'd be happy to entertain competing perspectives.
Sure.
https://people.eecs.berkeley.edu/~wkahan/JAVAhurt.pdf
Written by William Kahan, also known as the Old Man of Floating-Point:
https://news.ycombinator.com/item?id=29042853 - An Interview with the Old Man of Floating-Point (1998)
Anyway, enough ranting for the day, but I found this really hard to focus on to read - almost felt like a science-kook type manifesto, though I know it is not.
I looked at some of his other writings and they all suffer this. I'm not sure what TeX template he's using, but it's bizarre. (IMO, of course.)
I was optimizing my assembler to the nth degree, but optimizing the algorithm is always going to be the real winner.
I think it speaks to an issue I see in the software engineering world where people assume that collaboration is for low-IQ people and all great innovation comes from some super-genius working on their own for long enough, and isn't required to share how they arrived at their findings. I'm sure the mythology of this algorithm is propelled somewhat by the enigmatic character of writing "what the fuck" next to adding the constant, implying a mystical element to its utility that was arrived at without needing to clarify it in some long boring research paper.
I've also seen a phenomenon where developers believe their code is so valuable and amazing that they don't share it; it's really ordinary code but useful to novices nonetheless. Some communities I participated in when I was younger (e.g. PS3 jailbreak scene) were very protective of their code for no reason. Executables were obfuscated, nothing was open source, and developers were very hostile or intentionally trolled you when asking questions about their software.
I've also seen the reverse - perhaps more. People need advice but don't want to share their code because they are embarrassed by it. They wrote it thinking "no one will ever see this".
By contrast I sell code, so I know it'll be seen by lots of people, so I tend to spend time making sure style is consistent, things are well named, and so on. But equally, to me, it's just code. Feel free to comment on it - there's always room for improvement.
Toxic, necessarily so.
The Green Hills Software C compiler had a fast square root algorithm of similar form:
(x >> 1) + {magic constant}
dating between 1983-1985 if I recall correctly. Also implemented to maximize floating point performance benchmarks, and again not drawn from academia. If my recollection is correct, that is one of the earliest known examples of the general technique and predates even the official IEEE 754 floating point specification which was not formally ratified until 1985 (but the standard was in development since 1977 and already de facto adopted by the time it was formally ratified).[1] https://www.beyond3d.com/content/articles/15/
[2] https://en.wikipedia.org/wiki/Stardent_Inc.#Ardent_Computer_...
So for further accuracy we can instead have:
conv.i = 0x5F1FFFF9 - ( conv.i >> 1 ); conv.f = 0.703952253f ( 2.38924456f - x * conv.f * conv.f ); return conv.f;
[from Wikipedia]
> Solve lighting equations
Is it the light or laser effect when the gun is fired ?
So the lights in this case are any lights in the scene, typically represented as a point of origin and a brightness.
> Brian Hook may have brought the algorithm from 3dfx to id Software.
One of the many reasons you should stay away from real world languages when talking about algorithms unless you are an expert in the language.
The code snippet that the article claims is C:
int32_t compute_magic(void) {
double sigma = 0.0450465;
double expression = 1.5 * pow(2.0, 23.0) * (127.0 - sigma);
int32_t i = expression;
return i;
}
which, as far as I can tell, is perfectly well-defined Abstract C.If you use a union, your code won't read or write out of bounds, since it will save space for the larger type. Ideally you would also specify _Float32_t and int32_t for some pseudoportability (at least before endianness gets involved), although I'm aware this was not available in 1999.
Otherwise, carry on.
Whether it is bad practice or not, people used to do this all the time, and so the compilers all tend to support type conversion via pointer and type punning via union. A lot of critical code in the real world would break if it suddenly didn’t work.
C++ bit-cast is brand new (C++20) and until now the suggestion in C++ was to use memcpy, and hope and pray it gets elided. That might be correct but boy is it ugly and gross.