Bool -> Int But Stupid In Go
Tags: go, performance, learning
Published: 2024-03-09 07:31:23

## Intro
Type conversions are very important in programming, because for the most part, types don't exist, it's all binary, so by creating the helpful feature of types, we need a way to break that sometimes through conversion.

Also, we go through 7 different ways to create the `bool` to `int` conversion, my favorite is number 7, best to last, but you can jump right there if you want.

## Bool To Int
Most languages have a way to convert a Boolean value to an integer. In python, you would use `int(value)`, in JavaScript you don't care, and the list goes on. Furthermore, converting from an integer to a Boolean value isn't hard, you would simply use `value != 0`. This gives us `true` for every number other than `0`, which is your choice if that's okay, you might want negatives to return `false`, or only `1` to return `true`, but those are all easy to implement.
Go doesn't have a great way to convert a `bool` to an `int`. There are plenty of [suggestions to fix this](https://github.com/golang/go/issues/45320). But it hasn't really been successful.

## Why Is This Important
The current way to convert a `bool` to an `int` would be to write a function like this.
```golang
func boolToInt(b bool) int {
    if b {
        return 1
    }
    return 0
}
```
Not too bad, just have to remember to write this in every project you create. It's just annoying, but there is more to that. Performance-wise, this isn't the fastest code.

## Fastest Code
For those that just want the most performant code, here it is, found from [this blog post](https://0x0f.me/blog/golang-compiler-optimization/). I don't know who originally wrote the faster code, but here is the code.
```golang
func Bool2int(b bool) int {
	// The compiler currently only optimizes this form.
	// See issue 6011.
	var i int
	if b {
		i = 1
	} else {
		i = 0
	}
	return i
}
```
The link above goes into why this the optimized assembly is faster (it basically gets rid of a jump).
I don't really know why this gets optimized better and the first code doesn't, but I have a few guesses.

1. It's dumber, and that's all.
2. Putting the value into a variable and _then_ returning could play a part. This could be allowing the compiler to think about the value easier.

Otherwise, no clue, but someone probably explains it on [GitHub](https://github.com/golang/go/issues?q=is%3Aissue+6011+is%3Aclosed).

## How Branches Effect Performance
Ever heard of branchless programming? It's rarely brought up, mostly because the performance gain is usually not great enough to warrant slightly harder to read code. This doesn't mean it should be neglected, however, as it is extremely useful to know.
The main premise is that branches are slow, because the computer can't predict and get ready for what happening next. So, instead of using a branch, we just compute both paths and do extra calculations to get the correct value. The extra calculations should obviously always take less time than the computer getting back on track due to an incorrect branch prediction.

## Way 3, The Switch
We already know way 1, the if statement, and way 2, the dumb if statement. What about switch statements?
```golang
func convertViaSwitch(b bool) int {
	switch b {
	case true:
		return 1
	case false:
		return 0
	default:
		panic("this doesn't really need to exist")
	}
}
```
Originally, I wrote here that way 3 runs in _exactly_ the same amount of time as way 1, but I was wrong, it's actually _faster_. By such a miniscule amount, but still, it's actually faster (or at least from my testing it is. Although my testing is flawed, it _consistently_ performs better.

Also, the compiler doesn't think the 2 case statements are exhaustive, so you need a default or something similar. I have no idea if this is actually an issue, as you should never _really_ be using a switch on a `bool`.

## Way 4 & 5, Using A Map
I was actually surprised about this one, but this is _way_ slower than every other method.
```golang
func convertViaMap(b bool) int {
	table := map[bool]int{
		true:  1,
		false: 0,
	}
	return table[b]
}
```
Now I know what you're thinking, the function defines the table every call, so of course it's going to be slow, and you're right. So for way 5 we only define it once.
```golang
var table = map[bool]int{
	true:  1,
	false: 0,
}

func convertViaMap(b bool) int {
	return table[b]
}
```
And this is still extremely slow. I don't know where I got this idea, but in my mind, switch statements and a well optimized map should basically be equivalent, but apparently not in this case, so definitely don't use either way 4 or 5.

## Way 6, Using A List
As much as the map being slow confused me, this being faster confused me even more.
```golang
var list = []bool{
	false,
	true,
}

func convertViaList(b bool) int {
	return slices.Index(list, b)
}
```
Firstly, we're using a searching algorithm to go through the slice and find a match, so I expected doing all that would take extra time. Turns out it's not that much slower than way 1.

## Way 7, fastInvSqrt
Have you ever heard of the fast inverse square root algorithm? It was an algorithm used in Quake III, I'm pretty sure it was used for lighting and shading, but here's a great [video](https://www.youtube.com/watch?v=p8u_k2LIZyo) explaining it in detail. Here is that algorithm in C.
```c
float fastInvSqrt(float x) {
  int i = *(int*)&x;
  i = 0x5f3759df - (i >> 1);
  float y = *(float*)&i;
  return y * (1.5F - 0.5F * x * y * y);
}
```
I won't go into detail about it, but I'll explain the premise. In the first line we convert x into an integer bit for bit, which is important for later. The next few lines are explained better in the video, so I'm not even going to try. If you followed along with [Cosplore3D](https://dev.to/chigbeef_77/wolfenstein-3d-raycaster-cosplore3d-pt1-2iop), you might recall we actually used this algorithm, but translated to Go. I got that algorithm from [this repo](https://github.com/itchyny/fastinvsqrt), which has the fast inverse square root in many languages. I also found [this repo](https://github.com/arccoza/go-fastinvsqrt/blob/master/fis.go) with another implementation that I will definitely be checking out.
So how is this useful for us? Well let me show you the code I ended up with.
```golang
func fastBoolConv(b bool) int {
	return int(*(*byte)(unsafe.Pointer(&b)))
}
```
Remember, Booleans are stored as a 0 or a 1, it's just about getting the compiler to recognize the correct typing to that 0 or 1. Firstly, we create an unsafe pointer to `b`. The reason it's unsafe is because that pointer has no information about the type, it's just a pointer. This means we can reinterpret this pointer as a `byte` pointer, and we can indirect that so we have 0 or 1 but as a byte. Lastly, we convert to an `int`. I know you're wondering why I don't replace `byte` with `int` to get rid of a conversion, but it was actually _slower_ when I did that, so I'm leaving it like this even though I don't really understand. Is this solution practical though? And the answer is yes, it is. It's _almost_ as fast as way 2, but heaps faster than way 1, so I wouldn't complain if I started seeing this in codebases.

## What Do You Do With This Info
As with many of my posts, the takeaway is that it's not that important, however, check out these numbers! (Billion iterations, time in milliseconds). Also note that 4 and 5 are missing because they're stupidly slow.
```
way 1 6449ms
way 2 2868ms
way 3 6378ms
way 6 7268ms
way 7 2987ms
```
So I wasn't lying when I said that there was a performance gain, it's just a question as to whether it's worth it or not. Please use way 7 in production.