Preetham's Posts
https://www.preethamrn.com/
I write about productivity, software development, cubing, and various projects that I'm working on.Thu, 25 Aug 2022 10:53:22 GMThttps://validator.w3.org/feed/docs/rss2.htmlGridsome Feed Plugin<![CDATA[How Fast Inverse Square Root actually works]]>
https://www.preethamrn.com/posts/fast-inverse-sqrt-sfw/
https://www.preethamrn.com/posts/fast-inverse-sqrt-sfw/Sun, 14 Aug 2022 00:00:00 GMT> 1 ); // what ???
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // optional 2nd iteration
return y;
}
```
Fast Inverse Square Root is one of the most famous algorithms in game development. But what makes it so iconic? How does the algorithm work? And where does `0x5f3759df` come from? All will be answered in this "simple" blog post.
## Why is this algorithm so iconic?
It's not often that you see such vague comments in official, [public source code](https://github.com/id-Software/Quake-III-Arena/blob/dbe4ddb10315479fc00086f08e25d968b4b43c49/code/game/q_math.c#L552).[^1] And doing division without a single division operator! How's that even possible?! And more importantly, why?
Finding the inverse square root of a number is important for normalizing vectors in computer graphics programs which is often required in lighting and shaders calculations. These computations are made thousands of times per frame so it was imperative to find a fast algorithm for them.
To naively find the inverse square root we must first find the square root of a number and then find its reciprocal. Both of those are complex operations that take a long time on old CPUs. On the other hand, the fast algorithm only requires multiplications, bit shifts, and subtraction, all of which can run much faster so it became the defacto method for computing inverse square roots.
Is it used today? Not really. Hardware advancements have made this pretty obsolete since many CPUs come with rsqrt instructions which can compute the inverse square root in a single instruction[^2].
# "Slow inverse square root"
```c
float S_rsqrt( float number, int iterations ) {
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = 0.01; // initial value of y - the result that we're approximating
for (int i = 0; i < iterations; i++) {
y = y * ( threehalfs - ( x2 * y * y ) );
}
return y;
}
```
Here's my "slow" inverse square root algorithm.
Try [running](https://replit.com/@PreethamNaraya1/Slow-Inverse-Square-Root#main.c) it. It's slower but surprisingly it still works. Unlike the fast method, this doesn't use `0x5f3759df` or the "evil floating point hack". But it also doesn't use any square root or division operations. That's because those steps aren't required. The core of this algorithm is using something called Newton's method.
## Newton's Method
There are plenty of great resources on what this method is and why it works.
[Newton's method produces this fractal, why don't we teach it in calculus classes?](https://youtu.be/-RdOwhmqP5s?t=336)
TL;DW: It works by taking an approximation and iterating closer and closer to the actual value by riding the slope of the curve.
* The blue line is the equation for which we're trying to find the solution (the point where it intersects with the x-axis).
* The red line is the tangent to the blue line at the point where x is our initial guess ($y_n$). This is the slope that we're riding.
* The green line is the x intercept of the red line. We can either use this as our solution approximation or use it to repeat the Newton method with another guess ($y_{n+1}$) until we get close to the actual solution.
Here's a bunch of fancy math for completion's sake however you can skip to the [next section](#what--ie-choosing-a-better-initial-guess) if you're more interested in where `0x5f3759df` comes from and how the evil floating point bit level hack works.
### Fancy math
Let's say that x is our input number and y is the inverse square root. We want to solve for the equation
$$
\begin{aligned}
y &= 1/sqrt(x)\\
\text{or } 0 &= 1/y^2 - x
\end{aligned}
$$
Newton's method can help us solve the roots of this equation for y. Remember that we're solving for y here. x is a constant input.
$$
\begin{aligned}
f(y) &= 1/y^2 - x \\
f'(y) &= -2y^{-3}
\end{aligned}
$$
To get the next iteration of y, we "ride the slope" of f(y) one step closer to its root.
$$
\begin{aligned}
y_{next} &= y - f(y)/f'(y)\\
y_{next} &= y - \frac{1/y^2 - x}{-2/y^3}\\
y_{next} &= y + y/2 -xy^3/2\\
y_{next} &= y(3/2 -xy^2/2)
\end{aligned}
$$
Which is how we get the code
```c
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = y * (threehalfs - x2 * y * y) // first iteration
```
And now we're doing an inverse square root without a single division operator! Isn't that exciting!
The important thing to note here is that Newton's method is just an approximation. The closer your initial guess, the fewer iterations you'll need.[^3] With "slow inverse square root" we often need more than 10 iterations to converge on the actual value. In the fast inverse square root algorithm, we get away with just a single iteration. So that's our next goal - choosing a better initial guess.
# "What ???" ie, choosing a better initial guess
```cpp
i = 0x5f3759df - ( i >> 1 )
```
The `i` on the left hand side is our initial guess `y` and the `i` on the right hand side is our original number `x`. So let's rewrite the code so we don't get confused between the two different values of `i`.
```cpp
y_bits = 0x5f3759df - ( x_bits >> 1 )
```
Note that we're using $x_{bits}$ instead of $x$ here. "What's the difference between $x_{bits}$ and $x$?" you might ask. While $x$ is the actual number that we're computing the inverse square root for, $x_{bits}$ is the number that a computer stores internally to represent that number, that is, the **binary representation** of that number. For example, instead of $3.33$ we're using $01000000010101010001111010111001_{\text{base } 2}$
Using the binary representation allows us to do operations like subtraction (`-`) and bit shifting (`>>`). How we do this conversion will be explained in the [next section](#evil-floating-point-bit-level-hack) on "evil floating point bit level hacking" but first we need to understand how computers store numbers...
## How computers store numbers
Decimal integers use digits from 0 to 9 to represent numbers in base 10. Computers run off of 1s and 0s and so are restricted to only using base 2.
The 1s and 0s in a computer are known as bits. Grouping together bits allows us to represent larger numbers and the numbers that we'll be dealing with today have 32 bits.
Just like decimal integers use powers of 10 for each place (unit, tens, hundreds, thousands, etc.), binary integers use powers of 2. So:
* Decimal $1234 = 1 * 10^3 + 2 * 10^2 + 3 * 10 + 4$
* Binary $101101 = 1 * 2^5 + 0 * 2^4 + 1 * 2^3 + 1 * 2^2 + 0 * 2 + 1$
You may notice however, that this doesn't allow us to represent numbers with a decimal point in them like $1.5$ or $74.123$. For that, we need to use [The IEEE Floating Point Standard](#the-ieee-floating-point-standard)
### The IEEE Floating Point Standard
Floating point is a fancy way of saying binary scientific notation[^4].
Just like regular scientific notation has numbers like $+1.6*10^{15}, -1.731*10^{-52}, +4.25*10^0$, floating point has numbers like $+1.101011*2^{11010}, -1.001101*2^{-101}, -1.001*2^{0}$
There are a few commonalities in both representations:
1. The numbers are split into a sign (+ or -), a coefficient (also called a mantissa), and an exponent. For example, $-1.731*10^{-52}$ can be split into
* sign: $-$
* coefficient: $1.731$
* exponent: $-52$
2. The leading number is never zero. If it was, we could just shift the point to the first non-zero number and subtract from the exponent. For example, instead of $0.61*10^2$, we can write $6.1*10^1$
Using these two rules, we can write our floating point number as
$$
\begin{aligned}
x &= s*m*2^e
\end{aligned}
$$
To store this on a computer, we need to convert the $s$, $e$, and $m$ values into their binary representations `S`, `E`, and `M`. 1 bit for the sign, 8 bits for the exponent, and 23 bits for the mantissa to make 32 bits in total.
![IEEE 754 Standard](./ieee754-standard.png)
- s is the sign. If the sign bit `S` is 0 then the number is positive (ie, +1). 1 means negative (ie, -1). For the purposes of inverse square root x will always be positive (you can't take square roots of negative numbers in the "real" world), so `S` will always be 0. We can ignore it for the rest of this post.
- m is the mantissa. Since the leading digit of a floating point number is always a 1 in binary, the 1 is implied and `M` is just the fractional part after the point (ie, m = 1 + `M`) [^5]
- e is the exponent. To store positive and negative exponents, we take the unsigned 8 bit exponent value (`E`) and subtract 127 to get a range from -127 to +128. This allows us to store tiny fractions smaller than 1 using negative exponents and large numbers bigger than 1 using positive exponents.
Putting all those constraints together, we get the following equation for our floating point number x in terms of the binary representations of `S`, `M`, and `E`
$$
\begin{aligned}
x_{bits} &= 2^{23}*(E+M)\\
x &= S*(1 + M)*2^{E-127}
\end{aligned}
$$
Try playing around with this floating point number calculator to create floating point numbers of your own!
If you click on the scientific notation you'll notice that the scientific notation matches the input number even though they don't look anything alike.
### Working with logarithms
Working with exponents is tricky and confusing. Instead, by taking the logarithm, we turn confusing division, multiplication, and exponent operations into simple subtraction, addition, and multiplication.
It turns out that working with logarithms also allows us to find a relationship between the binary representation of x ($x_{bits}$) and the number $x$.
If you squint really hard then you can see that taking the log of x will bring the exponent value down and with some scaling and shifting, it's proportional to $x_{bits}$. Fortunately, we don't have to squint.
$$
\begin{aligned}
x_{bits} &= 2^{23}*(E + M) && \text{from earlier}
\end{aligned}
$$
Meanwhile
$$
\begin{aligned}
x &= (1 + M)*2^{E-127} && \text{from earlier}\\
\implies log_2(x) &= E - 127 + log(1+M)
\end{aligned}
$$
Through another fortunate quirk of logarithms, we see that [$x \approxeq log(1+x)$](https://www.desmos.com/calculator/k7eekdct1s) for small values of x between 0 and 1.
Since `M` will always be within 0 and 1, we can say that $M = log(1+M) + \varepsilon$ where $\varepsilon$ is a small error term.
![log(1+x) Approximation](./log-approximation.png)
Putting all of this together, we get
$$
\begin{aligned}
log_2(x) &= E-127+log(1+M)\\
&=E-127+M+\varepsilon\\
&=2^{23}*(E+M)/2^{23}-127+\varepsilon\\
&=x_{bits}/2^{23}-127+\varepsilon\\
\end{aligned}
$$
So now we have a mathematical relationship between the binary representation of x and log(x).
## What is `0x5f3759df`
Using logarithms allows us to turn $y = 1/x^{1/2}$ into $log(y) = -\frac{1}{2}log(x)$.
From here, we can use the relationship we found earlier to relate the binary representations of x and y.
$$
\begin{aligned}
&y = 1/x^{1/2}\\
\implies &log(y) = -\frac{1}{2}log(x)\\
\implies &y_{bits}/2^{23} - 127 + \varepsilon = -\frac{1}{2}(x_{bits}/2^{23} - 127 + \varepsilon)\\
\implies &y_{bits} = \frac{3}{2}2^{23}(127 -
ε) - x_{bits}/2
\end{aligned}
$$
Or in other words
```c
y_bits = 0x5f3759df - ( x_bits >> 1 );
```
$\frac{3}{2}2^{23}(127 - \varepsilon)$ gets us the magic number `0x5f3759df` and $-x_{bits}/2$ gets us `-(x_bits >> 1)`
If we ignore the error term ε and plug the magic number equation into [WolframAlpha](https://www.wolframalpha.com/input?i=%5Cfrac%7B3%7D%7B2%7D2%5E%7B23%7D%28127%29) we get 1598029824. And that's [equal to](https://www.wolframalpha.com/input?i=%5Cfrac%7B3%7D%7B2%7D2%5E%7B23%7D%28127%29+in+hex) … `0x5f400000`? So where did they get `0x5f3759df` from?…
Most likely from the ε… I guess we're going on another tangent.
## Optimizing ε with Minimaxing
Minimaxing is a lot like what it sounds like. In this case, we want to minimize the maximum error - in other words, find the magic number for which `Q_rsqrt` gives the smallest error compared to the actual inverse square root when considering all possible values of x_bits.
Since there are about 2 billion values of x and another 4 billion values for the magic number, we'll need to do some optimization if we want this to finish running before the sun consumes the solar system. Let's try speeding things up by cutting down the number of values that we need to search through.
1. In the previous step, we approximately narrowed down the magic number to `0x5f400000`. So we only need to search between `0x5f300000` and `0x5f500000`.
2. Instead of searching all values of x, we can ignore the exponent and only search for all values of the mantissa because ε only comes up in the equation $\text{M} = log(1 + \text{M}) + \varepsilon$. If we optimize ε for one exponent value, it's optimized for all exponent values.
3. Instead of searching all values of the magic number one by one, we can narrow down the value of the magic number digit by digit, working in increments of 0x10000, then 0x1000 and so on until all digits are found. This way, we only check around 160 values instead of 2 million.
That gives us the following pseudocode:
```c
// let's call the magic number C
// and the range of values we're checking is between cMin and cMax.
cMin, cMax = 0x5f300000, 0x5f500000
delta = 0x10000
while (delta > 0):
minMaxError, minMaxC = 10000, cMin
for each C between cMin and cMax in increments of delta:
for each mantissa value M:
x = 0x3f000000 + M // x in [0.5,2) with mantissa M
y = Q_rsqrt(x, C)
z = sqrt(x)
error = abs(1 - y * z) // relative error
if (error > minMaxError):
minMaxError = error
minMaxC = C
// narrow down the range of cMin to cMax
// and use smaller increments for delta
cMin = minMaxC - delta
cMax = minMaxC + delta
delta = delta >> 4
return minMaxC
```
[Try running the actual code for yourself](https://replit.com/@PreethamNaraya1/Minimaxing#main.c). You can try playing around with different ranges of values, different deltas, or different numbers of iterations to see how that impacts the result.
And now we get... `0x5f375a87`. This is still quite different from the constant found in the original code. At this point I was stumped. I got an answer but it wasn't the answer I was looking for. How did the developers come up with `0x5f3759df`?
I tried comparing the errors to see if our magic number was somehow producing worse results.
```bash
$ ./main --iterations=1 0x5f3759df
Max Error for 0x5f3759df: 0.00175233867209800831
$ ./main --iterations=1 0x5f375a87
Max Error for 0x5f375a87: 0.00175128778162259024
```
The error for our magic number `0x5f375a87` is smaller.
I tried it with 0 iterations of Newton's method
```bash
$ ./main --iterations=0 0x5f3759df
Max Error for 0x5f3759df: 0.03437577281600123769
$ ./main --iterations=0 0x5f375a87
Max Error for 0x5f375a87: 0.03436540281256528218
```
We're still smaller. I had to run it with 4 iterations of Newton's method before I started seeing both constants giving the same error of 0.00000010679068984665. And even then, the two constants were performing equally well.
So if `0x5f375a87` works better then why does Quake use `0x5f3759df`? Perhaps `0x5f3759df` works better with the numbers that Quake deals with. Perhaps the developer used a different method to generate this number. Perhaps the developer figured that their number worked well enough and didn't bother optimizing it further. Perhaps it was simply pulled out of the developer's rear. Only the person who wrote this code knows why `0x5f3759df` was chosen instead. At least now we know how the magic number works.[^6]
# Evil floating point bit level hack
```c
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
...
y = * ( float * ) &i;
```
In order to do the magic from the previous step, we need to work with the binary representation of numbers (`x_bits` and `y_bits`) instead of the floating point numbers (`x` and `y`) themselves. This requires us to convert from the floating point number `x` to the 32 bits that a computer uses to store that number internally. Those 32 bits are called a long int or long for short.
C allows you to convert between [floats](#the-ieee-floating-point-standard) and [longs](#how-computers-store-numbers) using [type casting](https://en.wikipedia.org/wiki/Type_conversion#C-like_languages). However, if you type cast a float to a long normally, then you would do the sensible thing and, for example, convert a float storing 3.33 into a integer storing 3.
The binary representation of float(3.33) is `0x40551eb9`
The binary representation of long(3) is `0x00000003`
Clearly these are very different and wouldn't help us when our equation from the previous step depends on `x_bits`. What we instead want is a long that's storing `0x40551eb9` (1079320249 in decimal).
In order to do that, we need to trick the computer into interpreting the floating point bits as long bits. We can do this by
1. telling the computer that this float pointer (`&y`)
2. is actually a long pointer (type casting using `(long *)`)
3. and then dereferencing that value into a long variable (`*`).
![C Memory Management](./c-memory.png)
That's what this line is doing (reading right to left): `i = * (long *) &y;`
Going back from i to y is just a reverse of the previous steps: convert the long pointer (`&i`) into a float pointer (`(float *)`) and dereferencing that value into a float variable (`*`). So we get `y = * ( float * ) &i;`
# Putting it all together
Now that we know how the algorithm works and why it works, hopefully we can turn the code a bit clearer with better comments.
```c
float Q_rsqrt(float number)
{
// interpreting the float bits of the number as a long
// by casting the float pointer to a long pointer without
// modifying the bits
long x_bits = * ( long * ) &number;
// finding a better initial guess for the inverse sqrt
long y_bits = 0x5f3759df - ( x_bits >> 1 );
// interpreting the long bits of y_bits as a float
// by reversing the steps from earlier
float y = * ( float * ) &y_bits;
const float threehalfs = 1.5F;
float half_x = number * 0.5F;
y = y * ( threehalfs - ( half_x * y * y ) ); // 1st iteration
// optional 2nd iteration to get a better approximation
// y = y * ( threehalfs - ( half_x * y * y ) );
return y;
}
```
To recap, the big leaps of logic for me were:
- Using Newton's method to do divisions using multiplication operations.
- Realizing the relationship between the floating point bit representation of x and log(x).
- Using log(x) and some algebra to get a close approximation for y.
- Using minimaxing to find a better magic number that accounts for the error term.
- Using pointer magic to interpret the bits of a float as a long and vice-versa.
When I started looking into this topic I didn't think it would lead me to calculus, solving optimization problems, the binary representation of floating point numbers, and memory management inside computers. I think that's what I enjoyed most about it. Any one of these ideas is interesting and many students learn about them every year, but to put them all together to solve a completely unrelated problem in vector graphics requires someone with a very specific set of skills.
![Venn Diagram](./venn-diagram.png)
What problems can you solve with your specific set of skills?
[^1]: The [history](https://www.beyond3d.com/content/articles/8/) behind the algorithm is pretty interesting too. The algorithm was originally found in the source code of Quake III Arena, attributed to the iconic John Carmack however it was later discovered to predate the game.
[^2]: [This article](https://www.linkedin.com/pulse/fast-inverse-square-root-still-armin-kassemi-langroodi/) goes into more detail and shows benchmarks.
[^3]: This is technically not always true because there are cases where a good initial guess can send you off on a wild goose chase. The [3Blue1Brown video](https://youtu.be/-RdOwhmqP5s?t=524) explains this better. However, for the purposes of fast inverse square root, this assumption works well.
[^4]: The reason it's called floating point is because the point isn't fixed. It's able to "float" depending on what the exponent value is.
[^5]: Astute readers might notice that if the mantissa is 0 then we can't avoid a leading 0, the floating point standard handles this in an interesting way but since the inverse of 0 is undefined, we'll just ignore it for the rest of this post. Even more astute readers would notice that the IEEE floating point standards includes denormalization where the leading 1 is excluded if all exponent bits at set to 0. Since that only happens for extremely small numbers, it's unlikely to cause issues in real world applications.
[^6]: Brute forcing the magic number by trying out all the different constants might be a bit unsatisfying for you. Maybe you wanted a mathematically rigorous way to narrow it down to the precise bit. The math is a bit out of scope for this article. However, there are some great papers by Chris Lomont and others that prove this (and find even better constants) using a lot of algebra and piecewise equation optimizations if you're into that stuff. See [Fast Inverse Square Root - Chris Lomont](http://www.lomont.org/papers/2003/InvSqrt.pdf) or [A Modification of the Fast Inverse Square Root Algorithm](https://www.preprints.org/manuscript/201908.0045/v1).]]><![CDATA[How Fast Inverse Square Root actually works]]>
https://www.preethamrn.com/posts/fast-inverse-sqrt/
https://www.preethamrn.com/posts/fast-inverse-sqrt/Sun, 14 Aug 2022 00:00:00 GMT This article contains some profanity which is found in the original code. If you'd prefer to read a version without profanity or one to show kids check out [the SFW version here](fast-inverse-sqrt-sfw).
$$
\frac{1}{\sqrt{x}}
$$
```c
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 ) ); // optional 2nd iteration
return y;
}
```
Fast Inverse Square Root is one of the most famous algorithms in game development. But what makes it so iconic? How does the algorithm work? And where does `0x5f3759df` come from? All will be answered in this "simple" blog post.
## Why is this algorithm so iconic?
It's not often that you see swear words in official, [public source code](https://github.com/id-Software/Quake-III-Arena/blob/dbe4ddb10315479fc00086f08e25d968b4b43c49/code/game/q_math.c#L552).[^1] And doing division without a single division operator! How's that even possible?! And more importantly, why?
Finding the inverse square root of a number is important for normalizing vectors in computer graphics programs which is often required in lighting and shaders calculations. These computations are made thousands of times per frame so it was imperative to find a fast algorithm for them.
To naively find the inverse square root we must first find the square root of a number and then find its reciprocal. Both of those are complex operations that take a long time on old CPUs. On the other hand, the fast algorithm only requires multiplications, bit shifts, and subtraction, all of which can run much faster so it became the defacto method for computing inverse square roots.
Is it used today? Not really. Hardware advancements have made this pretty obsolete since many CPUs come with rsqrt instructions which can compute the inverse square root in a single instruction[^2].
# "Slow inverse square root"
```c
float S_rsqrt( float number, int iterations ) {
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = 0.01; // initial value of y - the result that we're approximating
for (int i = 0; i < iterations; i++) {
y = y * ( threehalfs - ( x2 * y * y ) );
}
return y;
}
```
Here's my "slow" inverse square root algorithm.
Try [running](https://replit.com/@PreethamNaraya1/Slow-Inverse-Square-Root#main.c) it. It's slower but surprisingly it still works. Unlike the fast method, this doesn't use `0x5f3759df` or the "evil floating point hack". But it also doesn't use any square root or division operations. That's because those steps aren't required. The core of this algorithm is using something called Newton's method.
## Newton's Method
There are plenty of great resources on what this method is and why it works.
[Newton's method produces this fractal, why don't we teach it in calculus classes?](https://youtu.be/-RdOwhmqP5s?t=336)
TL;DW: It works by taking an approximation and iterating closer and closer to the actual value by riding the slope of the curve.
* The blue line is the equation for which we're trying to find the solution (the point where it intersects with the x-axis).
* The red line is the tangent to the blue line at the point where x is our initial guess ($y_n$). This is the slope that we're riding.
* The green line is the x intercept of the red line. We can either use this as our solution approximation or use it to repeat the Newton method with another guess ($y_{n+1}$) until we get close to the actual solution.
Here's a bunch of fancy math for completion's sake however you can skip to the [next section](#what-the-fuck-ie-choosing-a-better-initial-guess) if you're more interested in where `0x5f3759df` comes from and how the evil floating point bit level hack works.
### Fancy math
Let's say that x is our input number and y is the inverse square root. We want to solve for the equation
$$
\begin{aligned}
y &= 1/sqrt(x)\\
\text{or } 0 &= 1/y^2 - x
\end{aligned}
$$
Newton's method can help us solve the roots of this equation for y. Remember that we're solving for y here. x is a constant input.
$$
\begin{aligned}
f(y) &= 1/y^2 - x \\
f'(y) &= -2y^{-3}
\end{aligned}
$$
To get the next iteration of y, we "ride the slope" of f(y) one step closer to its root.
$$
\begin{aligned}
y_{next} &= y - f(y)/f'(y)\\
y_{next} &= y - \frac{1/y^2 - x}{-2/y^3}\\
y_{next} &= y + y/2 -xy^3/2\\
y_{next} &= y(3/2 -xy^2/2)
\end{aligned}
$$
Which is how we get the code
```c
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = y * (threehalfs - x2 * y * y) // first iteration
```
And now we're doing an inverse square root without a single division operator! Isn't that exciting!
The important thing to note here is that Newton's method is just an approximation. The closer your initial guess, the fewer iterations you'll need.[^3] With "slow inverse square root" we often need more than 10 iterations to converge on the actual value. In the fast inverse square root algorithm, we get away with just a single iteration. So that's our next goal - choosing a better initial guess.
# "What the fuck?" ie, choosing a better initial guess
```cpp
i = 0x5f3759df - ( i >> 1 )
```
The `i` on the left hand side is our initial guess `y` and the `i` on the right hand side is our original number `x`. So let's rewrite the code so we don't get confused between the two different values of `i`.
```cpp
y_bits = 0x5f3759df - ( x_bits >> 1 )
```
Note that we're using $x_{bits}$ instead of $x$ here. "What's the difference between $x_{bits}$ and $x$?" you might ask. While $x$ is the actual number that we're computing the inverse square root for, $x_{bits}$ is the number that a computer stores internally to represent that number, that is, the **binary representation** of that number. For example, instead of $3.33$ we're using $01000000010101010001111010111001_{\text{base } 2}$
Using the binary representation allows us to do operations like subtraction (`-`) and bit shifting (`>>`). How we do this conversion will be explained in the [next section](#evil-floating-point-bit-level-hack) on "evil floating point bit level hacking" but first we need to understand how computers store numbers...
## How computers store numbers
Decimal integers use digits from 0 to 9 to represent numbers in base 10. Computers run off of 1s and 0s and so are restricted to only using base 2.
The 1s and 0s in a computer are known as bits. Grouping together bits allows us to represent larger numbers and the numbers that we'll be dealing with today have 32 bits.
Just like decimal integers use powers of 10 for each place (unit, tens, hundreds, thousands, etc.), binary integers use powers of 2. So:
* Decimal $1234 = 1 * 10^3 + 2 * 10^2 + 3 * 10 + 4$
* Binary $101101 = 1 * 2^5 + 0 * 2^4 + 1 * 2^3 + 1 * 2^2 + 0 * 2 + 1$
You may notice however, that this doesn't allow us to represent numbers with a decimal point in them like $1.5$ or $74.123$. For that, we need to use [The IEEE Floating Point Standard](#the-ieee-floating-point-standard)
### The IEEE Floating Point Standard
Floating point is a fancy way of saying binary scientific notation[^4].
Just like regular scientific notation has numbers like $+1.6*10^{15}, -1.731*10^{-52}, +4.25*10^0$, floating point has numbers like $+1.101011*2^{11010}, -1.001101*2^{-101}, -1.001*2^{0}$
There are a few commonalities in both representations:
1. The numbers are split into a sign (+ or -), a coefficient (also called a mantissa), and an exponent. For example, $-1.731*10^{-52}$ can be split into
* sign: $-$
* coefficient: $1.731$
* exponent: $-52$
2. The leading number is never zero. If it was, we could just shift the point to the first non-zero number and subtract from the exponent. For example, instead of $0.61*10^2$, we can write $6.1*10^1$
Using these two rules, we can write our floating point number as
$$
\begin{aligned}
x &= s*m*2^e
\end{aligned}
$$
To store this on a computer, we need to convert the $s$, $e$, and $m$ values into their binary representations `S`, `E`, and `M`. 1 bit for the sign, 8 bits for the exponent, and 23 bits for the mantissa to make 32 bits in total.
![IEEE 754 Standard](./ieee754-standard.png)
- s is the sign. If the sign bit `S` is 0 then the number is positive (ie, +1). 1 means negative (ie, -1). For the purposes of inverse square root x will always be positive (you can't take square roots of negative numbers in the "real" world), so `S` will always be 0. We can ignore it for the rest of this post.
- m is the mantissa. Since the leading digit of a floating point number is always a 1 in binary, the 1 is implied and `M` is just the fractional part after the point (ie, m = 1 + `M`) [^5]
- e is the exponent. To store positive and negative exponents, we take the unsigned 8 bit exponent value (`E`) and subtract 127 to get a range from -127 to +128. This allows us to store tiny fractions smaller than 1 using negative exponents and large numbers bigger than 1 using positive exponents.
Putting all those constraints together, we get the following equation for our floating point number x in terms of the binary representations of `S`, `M`, and `E`
$$
\begin{aligned}
x_{bits} &= 2^{23}*(E+M)\\
x &= S*(1 + M)*2^{E-127}
\end{aligned}
$$
Try playing around with this floating point number calculator to create floating point numbers of your own!
If you click on the scientific notation you'll notice that the scientific notation matches the input number even though they don't look anything alike.
### Working with logarithms
Working with exponents is tricky and confusing. Instead, by taking the logarithm, we turn confusing division, multiplication, and exponent operations into simple subtraction, addition, and multiplication.
It turns out that working with logarithms also allows us to find a relationship between the binary representation of x ($x_{bits}$) and the number $x$.
If you squint really hard then you can see that taking the log of x will bring the exponent value down and with some scaling and shifting, it's proportional to $x_{bits}$. Fortunately, we don't have to squint.
$$
\begin{aligned}
x_{bits} &= 2^{23}*(E + M) && \text{from earlier}
\end{aligned}
$$
Meanwhile
$$
\begin{aligned}
x &= (1 + M)*2^{E-127} && \text{from earlier}\\
\implies log_2(x) &= E - 127 + log(1+M)
\end{aligned}
$$
Through another fortunate quirk of logarithms, we see that [$x \approxeq log(1+x)$](https://www.desmos.com/calculator/k7eekdct1s) for small values of x between 0 and 1.
Since `M` will always be within 0 and 1, we can say that $M = log(1+M) + \varepsilon$ where $\varepsilon$ is a small error term.
![log(1+x) Approximation](./log-approximation.png)
Putting all of this together, we get
$$
\begin{aligned}
log_2(x) &= E-127+log(1+M)\\
&=E-127+M+\varepsilon\\
&=2^{23}*(E+M)/2^{23}-127+\varepsilon\\
&=x_{bits}/2^{23}-127+\varepsilon\\
\end{aligned}
$$
So now we have a mathematical relationship between the binary representation of x and log(x).
## What is `0x5f3759df`
Using logarithms allows us to turn $y = 1/x^{1/2}$ into $log(y) = -\frac{1}{2}log(x)$.
From here, we can use the relationship we found earlier to relate the binary representations of x and y.
$$
\begin{aligned}
&y = 1/x^{1/2}\\
\implies &log(y) = -\frac{1}{2}log(x)\\
\implies &y_{bits}/2^{23} - 127 + \varepsilon = -\frac{1}{2}(x_{bits}/2^{23} - 127 + \varepsilon)\\
\implies &y_{bits} = \frac{3}{2}2^{23}(127 -
ε) - x_{bits}/2
\end{aligned}
$$
Or in other words
```c
y_bits = 0x5f3759df - ( x_bits >> 1 );
```
$\frac{3}{2}2^{23}(127 - \varepsilon)$ gets us the magic number `0x5f3759df` and $-x_{bits}/2$ gets us `-(x_bits >> 1)`
If we ignore the error term ε and plug the magic number equation into [WolframAlpha](https://www.wolframalpha.com/input?i=%5Cfrac%7B3%7D%7B2%7D2%5E%7B23%7D%28127%29) we get 1598029824. And that's [equal to](https://www.wolframalpha.com/input?i=%5Cfrac%7B3%7D%7B2%7D2%5E%7B23%7D%28127%29+in+hex) … `0x5f400000`? So where did they get `0x5f3759df` from?…
Most likely from the ε… I guess we're going on another tangent.
## Optimizing ε with Minimaxing
Minimaxing is a lot like what it sounds like. In this case, we want to minimize the maximum error - in other words, find the magic number for which `Q_rsqrt` gives the smallest error compared to the actual inverse square root when considering all possible values of x_bits.
Since there are about 2 billion values of x and another 4 billion values for the magic number, we'll need to do some optimization if we want this to finish running before the sun consumes the solar system. Let's try speeding things up by cutting down the number of values that we need to search through.
1. In the previous step, we approximately narrowed down the magic number to `0x5f400000`. So we only need to search between `0x5f300000` and `0x5f500000`.
2. Instead of searching all values of x, we can ignore the exponent and only search for all values of the mantissa because ε only comes up in the equation $\text{M} = log(1 + \text{M}) + \varepsilon$. If we optimize ε for one exponent value, it's optimized for all exponent values.
3. Instead of searching all values of the magic number one by one, we can narrow down the value of the magic number digit by digit, working in increments of 0x10000, then 0x1000 and so on until all digits are found. This way, we only check around 160 values instead of 2 million.
That gives us the following pseudocode:
```c
// let's call the magic number C
// and the range of values we're checking is between cMin and cMax.
cMin, cMax = 0x5f300000, 0x5f500000
delta = 0x10000
while (delta > 0):
minMaxError, minMaxC = 10000, cMin
for each C between cMin and cMax in increments of delta:
for each mantissa value M:
x = 0x3f000000 + M // x in [0.5,2) with mantissa M
y = Q_rsqrt(x, C)
z = sqrt(x)
error = abs(1 - y * z) // relative error
if (error > minMaxError):
minMaxError = error
minMaxC = C
// narrow down the range of cMin to cMax
// and use smaller increments for delta
cMin = minMaxC - delta
cMax = minMaxC + delta
delta = delta >> 4
return minMaxC
```
[Try running the actual code for yourself](https://replit.com/@PreethamNaraya1/Minimaxing#main.c). You can try playing around with different ranges of values, different deltas, or different numbers of iterations to see how that impacts the result.
And now we get... `0x5f375a87`. This is still quite different from the constant found in the original code. At this point I was stumped. I got an answer but it wasn't the answer I was looking for. How did the developers come up with `0x5f3759df`?
I tried comparing the errors to see if our magic number was somehow producing worse results.
```bash
$ ./main --iterations=1 0x5f3759df
Max Error for 0x5f3759df: 0.00175233867209800831
$ ./main --iterations=1 0x5f375a87
Max Error for 0x5f375a87: 0.00175128778162259024
```
The error for our magic number `0x5f375a87` is smaller.
I tried it with 0 iterations of Newton's method
```bash
$ ./main --iterations=0 0x5f3759df
Max Error for 0x5f3759df: 0.03437577281600123769
$ ./main --iterations=0 0x5f375a87
Max Error for 0x5f375a87: 0.03436540281256528218
```
We're still smaller. I had to run it with 4 iterations of Newton's method before I started seeing both constants giving the same error of 0.00000010679068984665. And even then, the two constants were performing equally well.
So if `0x5f375a87` works better then why does Quake use `0x5f3759df`? Perhaps `0x5f3759df` works better with the numbers that Quake deals with. Perhaps the developer used a different method to generate this number. Perhaps the developer figured that their number worked well enough and didn't bother optimizing it further. Perhaps it was simply pulled out of the developer's rear. Only the person who wrote this code knows why `0x5f3759df` was chosen instead. At least now we know how the magic number works.[^6]
# Evil floating point bit level hack
```c
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
...
y = * ( float * ) &i;
```
In order to do the magic from the previous step, we need to work with the binary representation of numbers (`x_bits` and `y_bits`) instead of the floating point numbers (`x` and `y`) themselves. This requires us to convert from the floating point number `x` to the 32 bits that a computer uses to store that number internally. Those 32 bits are called a long int or long for short.
C allows you to convert between [floats](#the-ieee-floating-point-standard) and [longs](#how-computers-store-numbers) using [type casting](https://en.wikipedia.org/wiki/Type_conversion#C-like_languages). However, if you type cast a float to a long normally, then you would do the sensible thing and, for example, convert a float storing 3.33 into a integer storing 3.
The binary representation of float(3.33) is `0x40551eb9`
The binary representation of long(3) is `0x00000003`
Clearly these are very different and wouldn't help us when our equation from the previous step depends on `x_bits`. What we instead want is a long that's storing `0x40551eb9` (1079320249 in decimal).
In order to do that, we need to trick the computer into interpreting the floating point bits as long bits. We can do this by
1. telling the computer that this float pointer (`&y`)
2. is actually a long pointer (type casting using `(long *)`)
3. and then dereferencing that value into a long variable (`*`).
![C Memory Management](./c-memory.png)
That's what this line is doing (reading right to left): `i = * (long *) &y;`
Going back from i to y is just a reverse of the previous steps: convert the long pointer (`&i`) into a float pointer (`(float *)`) and dereferencing that value into a float variable (`*`). So we get `y = * ( float * ) &i;`
# Putting it all together
Now that we know how the algorithm works and why it works, hopefully we can turn the code a bit clearer with better comments.
```c
float Q_rsqrt(float number)
{
// interpreting the float bits of the number as a long
// by casting the float pointer to a long pointer without
// modifying the bits
long x_bits = * ( long * ) &number;
// finding a better initial guess for the inverse sqrt
long y_bits = 0x5f3759df - ( x_bits >> 1 );
// interpreting the long bits of y_bits as a float
// by reversing the steps from earlier
float y = * ( float * ) &y_bits;
const float threehalfs = 1.5F;
float half_x = number * 0.5F;
y = y * ( threehalfs - ( half_x * y * y ) ); // 1st iteration
// optional 2nd iteration to get a better approximation
// y = y * ( threehalfs - ( half_x * y * y ) );
return y;
}
```
To recap, the big leaps of logic for me were:
- Using Newton's method to do divisions using multiplication operations.
- Realizing the relationship between the floating point bit representation of x and log(x).
- Using log(x) and some algebra to get a close approximation for y.
- Using minimaxing to find a better magic number that accounts for the error term.
- Using pointer magic to interpret the bits of a float as a long and vice-versa.
When I started looking into this topic I didn't think it would lead me to calculus, solving optimization problems, the binary representation of floating point numbers, and memory management inside computers. I think that's what I enjoyed most about it. Any one of these ideas is interesting and many students learn about them every year, but to put them all together to solve a completely unrelated problem in vector graphics requires someone with a very specific set of skills.
![Venn Diagram](./venn-diagram.png)
What problems can you solve with your specific set of skills?
[^1]: The [history](https://www.beyond3d.com/content/articles/8/) behind the algorithm is pretty interesting too. The algorithm was originally found in the source code of Quake III Arena, attributed to the iconic John Carmack however it was later discovered to predate the game.
[^2]: [This article](https://www.linkedin.com/pulse/fast-inverse-square-root-still-armin-kassemi-langroodi/) goes into more detail and shows benchmarks.
[^3]: This is technically not always true because there are cases where a good initial guess can send you off on a wild goose chase. The [3Blue1Brown video](https://youtu.be/-RdOwhmqP5s?t=524) explains this better. However, for the purposes of fast inverse square root, this assumption works well.
[^4]: The reason it's called floating point is because the point isn't fixed. It's able to "float" depending on what the exponent value is.
[^5]: Astute readers might notice that if the mantissa is 0 then we can't avoid a leading 0, the floating point standard handles this in an interesting way but since the inverse of 0 is undefined, we'll just ignore it for the rest of this post. Even more astute readers would notice that the IEEE floating point standards includes denormalization where the leading 1 is excluded if all exponent bits at set to 0. Since that only happens for extremely small numbers, it's unlikely to cause issues in real world applications.
[^6]: Brute forcing the magic number by trying out all the different constants might be a bit unsatisfying for you. Maybe you wanted a mathematically rigorous way to narrow it down to the precise bit. The math is a bit out of scope for this article. However, there are some great papers by Chris Lomont and others that prove this (and find even better constants) using a lot of algebra and piecewise equation optimizations if you're into that stuff. See [Fast Inverse Square Root - Chris Lomont](http://www.lomont.org/papers/2003/InvSqrt.pdf) or [A Modification of the Fast Inverse Square Root Algorithm](https://www.preprints.org/manuscript/201908.0045/v1).]]><![CDATA[2020 Year in Review]]>
https://www.preethamrn.com/posts/2020-year-in-review/
https://www.preethamrn.com/posts/2020-year-in-review/Fri, 01 Jan 2021 00:00:00 GMT
It's a cubing competition between Feliks Zemdegs and Tymon Kolasiński. Feliks is the [world record holder of many Rubik's cube categories](https://www.worldcubeassociation.org/persons/2009ZEMD01). That was live streamed on Twitch with a peak of 1000+ concurrent viewers (and many more watching it after the fact). And the CubersLive logo is right there in the bottom left corner.
## TwistyPuzzleCup
The above story concurrently takes a different path at step 4.
4. Me and a few friends ([Manu](https://www.worldcubeassociation.org/persons/2013SING12) and [Michael](https://www.worldcubeassociation.org/persons/2016CHAI03)) decide to start our own online cubing competition.
Before jumping into it and coding a full fledged application we did a test run with Google Sheets, Google Forms and a super basic StreamLabs setup[^3]. [It went way better than I could have ever imagined](https://www.youtube.com/watch?v=eR9VmV4n_Oc) and so I went on a massive coding binge as I implemented a completely new frontend and backend in CubersLive (while simultaneously implementing new features required for MonkeyLeague) to support this competition while also not breaking any existing functionality. This ended up being some of the best Javascript code I've written so far[^4]. Finally we unveiled the [Twisty Puzzle Cup](https://www.cuberslive.com/tpc).
We managed to get sponsored by SpeedCubeShop and were able to have a prize pool for where we decided to do a full segment with four events - 3x3, 2x2, Pyraminx, and 3BLD. While the turnout wasn't as much as Cubing At Home or the MonkeyLeague, I felt ecstatic regardless because for the first I finally felt like a part of the cubing community instead of just an outside spectator[^5].
A month later we did a second segment partnering with SpeedCubeShop and the American Cancer Society [where we raised over $1700](https://tiltify.com/@cuberslive/cuberslive-vs-cancer) for charity. PogChamp
I also have to thank people like Dan, Evan, Ben, Tazzlyn, Tiffany, and any one else who help cast or participate in the stream or competition[^6]. It wouldn't have been possible without them.
## Twitch Affiliate
3. I decided to create a Twitch account and start streaming too.
If it seemed like I glossed over that point above, it's for good reason. It deserves its own section. At first I only played with friends and they kept me company on discord but then I started getting a few random people watching me regularly. Partly because they knew me from CubersLive and the Twisty Puzzle Cup, partly from me hanging out in other streamers' chats, and partly from me being lucky and people finding me on their own.
A few notable moments. Getting a random viewer out of nowhere on my first day streaming who ended up following and continues to watch me to this day. [Getting a huge raid followed by a personal best in a Super Mario 64 speedrun](https://www.youtube.com/watch?v=CkuTit7uLc4). And then [it happened again](https://www.youtube.com/watch?v=O06n_YQiF8w) on another stream weeks later. Doing almost 400 pushups in a single stream. Building a Bluetooth cube algorithm trainer. Beating the hardest game known to man[^7] - Pogostuck. And then beating an even harder Pogostuck map that fewer than 1000 people have beaten in the world.
Sometime in the middle of playing Pogostuck I ended up getting affiliate by meeting all the requirements.
![Twitch - Analytics](./twitch-analytics.png)
Looks like I'm still growing so I don't plan on stopping anytime soon. If you're interested, find me on [https://twitch.tv/preethamrn](https://twitch.tv/preethamrn) 🙂
## Rubik's Cube Alg Trainer
A fundamental part of solving a Rubik's cube is using algorithms to go from one state of the cube to a simpler state. The size of these alg sets range from 20 to 400 or even over 1000 depending on how much of the cube you'd like to solve in a single step. The best way to learn these algorithms is by simply practicing them over and over until they're in your muscle memory.
Usually this is done by scrambling the cube and then solving it a bunch of times, however, scrambling is long and time consuming. So naturally when I bought a Bluetooth cube, the first thing I wanted to do was build [my own alg trainer](https://www.preethamrn.com/cubing/algtrainer) which allowed you to continuously solve by simulating a virtual cube that would be automatically scrambled for you.
Through this I learned a lot about the cubing.js library, got another 2 weeks of content for my Twitch stream, and met a few people working on really interesting projects in the Rubik's cube space.
## Personal Website
The more I learn about web development, the more my personal website evolves. 5 years ago, I just copy and pasted a template that I found online. A few year later, I learned about Vue and built my own website from the ground up. It looked nice but as I started integrating more packages and utilities, the bundle size starting growing. Most people visiting my portfolio didn't need the code for Bluetooth cube interfaces, but since I built a single page application, they downloaded it anyway. Even though I hacked together a way to solve this, I figured
1. it's been long enough and I've learned a few new skills[^8]
2. I want to add a blog posts section to my website
so I rewrote it using [Gridsome](https://gridsome.org/). It's nice to use and makes writing efficient websites simple, however, whenever I had issues, it was difficult for me to debug them because the documentation was pretty lacking and the community is a lot smaller. Once I got everything working however, it's been pretty nice to work with and I think [the new website is a lot better than the old one](https://www.preethamrn.com/posts/test-post/).
## Other notable "doings"
### Stanz Sheet
I built a few webapps for a [Twitch streamer](https://www.twitch.tv/stanz).
1. [Valorant Player History](https://www.preethamrn.com/twitch-apps/valorant/) - A visualization of Valorant players and the other teams/players they are connected to ([Video](https://twitter.com/preethamrn/status/1298500209236795392)).
2. [The Stanz Sheet](https://www.preethamrn.com/twitch-apps/stanzsheet/) - A tool for taking notes about live Valorant matches ([Video](https://twitter.com/preethamrn/status/1298500210667134977?)).
### YouTube
I experimented with a few new YouTube video formats. I didn't upload as much this year as I did last year but I think the 100s of hours spend streaming on Twitch makes up for that. Additionally, I'd rather upload quality videos on YouTube. At the end of the day, it's not my career so the numbers don't really matter. Instead I prefer posting things that I can show to others as something I'm proud of[^9]. If they also get popular as a result, then that's even better.
### Notion
I switched from Todoist to Notion in July and it has already paid off multiple times when I needed to take notes but was at a different device or had to look back at what I was doing 2 months ago. My few gripes with it are that there are many simple bugs that aren't being fixed[^10], it doesn't have good support for recurring tasks or habit tracking, and the Android app is extremely slow and doesn't have offline support. I might talk about this in the future but I don't have enough to say to warrant a full post.
---
## Coronavirus
I'd be remiss if I didn't mention the one thing that I'm sure affected every single person reading this. Regardless of whether you had a great or horrible year, the pandemic probably contributed to a major part of that. I can't help but feel a little guilty when I have something good happen amidst such a terrible event for most people.
On one hand, I haven't met any friends in person for almost a year. On the other hand, I regularly speak with people from high school and college that I rarely talked to beforehand.
Communication is harder at work but I've also gotten much more time to work on coding and design, not to mention the fact that I also still have a job.
Without a commute I've lost a sense of schedule which has caused problems for work life balance and thrown a lot of my habits out of whack but I now have a few extra hours to work on personal projects and live streaming which led to me becoming a Twitch affiliate.
I don't have a way to end this section other than to say that I hope everyone stays safe and things get back to normal soon.
## Theme for 2021
In selecting my theme for 2021, I had two simple goals.
1. Continue building things just like I did this year.
2. Set myself up for 2022 - Year of Independence.
I think a good transition year would be the **Year of Habits**. Each of my goals supports a different category of habits. The "continue building things" habit would involve being more consistent with the time I spend working. Instead of having massive bursts of productivity every few weeks that end up dying before I'm able to publish, I'd prefer to slowly iterate on a few ideas that I think have promise. The "set myself up for 2022" habits would involve being less reliant on external factors like a rigid schedule or commute. If I can fix my sleep schedule, work out regularly, and read more (audiobooks count) while still living through a pandemic then things will only get better as the world goes back to normal.
## Conclusion
If there's one takeaway from this, it's to give themes a shot if New Year's Resolutions don't work for you. And if a year long theme is too much then just try it for a season. Like the Winter of Health.
It's probably a bad idea to write this much for my first blog post but on the bright side that means my writing is only going to get better and hopefully the 2021 Year in Review post will show that.
I'll end this off with a photo from the start of the year vs. the end of the year.
![Year Start versus End Pics](./pics.png)
[^1]: I did something sort of like themes before this but it wasn't until I was introduced to this idea that I actually put it in words. Before I had quarterly goals like learn cooking, make friends, and work out more whereas now I would call that Year/Season of Health.
[^2]: I think it had something to do with the original mod trying to sell the subreddit and discord. In order to regain access the other mods had to organize a coup and petition access with the reddit admins. Since I don't know all the details I don't want to say more at risk of butchering the story.
[^3]: If there's one thing I learned for this experience, it's the importance of testing an MVP. After the test run, we nailed down a lot of details that we weren't sure of and those design decisions fed into the final application. Also, if the test run didn't work out as well as we expected, then it would have been smarter to give up or try something different instead of wasting weeks on an idea that was doomed to fail.
[^4]: This isn't saying much considering it's my latest big Javascript project and as long as I keep learning new things, my newer code is going to naturally get better.
[^5]: Something that my intro programming class professor told that I always try to keep to heart is to be a creator and not just a consumer. I leaned a lot in that class but this is the one thing that I remember the most.
[^6]: If I missed your name let me know and I'll be happy to add you.
[^7]: hyperbole
[^8]: see above projects.
[^9]: I've actually had people I meet mention my YouTube channel without me bringing it up (they probably Googled my name) and said some of the videos were interesting.
[^10]: For example, you can't change the text color of captions to be black. Horizontal dividers disappear when there is a content block preceding them. These seem like one line CSS fixes but it seems like no one in the Notion team is taking ownership of these bug fixes.
]]><![CDATA[Test Post Please Ignore]]>
https://www.preethamrn.com/posts/test-post/
https://www.preethamrn.com/posts/test-post/Wed, 11 Nov 2020 00:00:00 GMT