DEV Community

Chig Beef
Chig Beef

Posted on

Bool -> Int But Stupid In Go

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. 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.

func boolToInt(b bool) int {
    if b {
        return 1
    }
    return 0
}
Enter fullscreen mode Exit fullscreen mode

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. I don't know who originally wrote the faster code, but here is the code.

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
}
Enter fullscreen mode Exit fullscreen mode

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.

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?

func convertViaSwitch(b bool) int {
    switch b {
    case true:
        return 1
    case false:
        return 0
    default:
        panic("this doesn't really need to exist")
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

func convertViaMap(b bool) int {
    table := map[bool]int{
        true:  1,
        false: 0,
    }
    return table[b]
}
Enter fullscreen mode Exit fullscreen mode

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.

var table = map[bool]int{
    true:  1,
    false: 0,
}

func convertViaMap(b bool) int {
    return table[b]
}
Enter fullscreen mode Exit fullscreen mode

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.

var list = []bool{
    false,
    true,
}

func convertViaList(b bool) int {
    return slices.Index(list, b)
}
Enter fullscreen mode Exit fullscreen mode

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 explaining it in detail. Here is that algorithm in 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);
}
Enter fullscreen mode Exit fullscreen mode

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, you might recall we actually used this algorithm, but translated to Go. I got that algorithm from this repo, which has the fast inverse square root in many languages. I also found this repo 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.

func fastBoolConv(b bool) int {
    return int(*(*byte)(unsafe.Pointer(&b)))
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)