Handmade Hero»Forums»Code
Jordan Duval
21 posts
What is the deal with Casey using bool32?
What is the deal with Casey using bool32? Maybe I just missed him explaining it but when he began going typedef crazy with all of the ints, uints, floats, of all different size he then changed all of his bools to bool32s which are really just int32s.

Correct me if I am wrong but doesn't it make more sense to use a bool that occupy less space, i.e. just less overhead? Or is there a method to his madness?

Thanks,

Jordan
Bryan Taylor
55 posts
What is the deal with Casey using bool32?
For the most part, it's a more accurate representation of what's actually happening on the CPU.

As a local, a bool is going to either be in a register (in which case the size is irrelevant), or on the stack, in which case it's bound by stack alignment and is going to take up more than a byte anyway.

In a struct, the alignment is probably going to cost more than a byte as well:
1
2
3
4
5
struct Foo {
  s64 x;  // Forces alignment to 8
  bool b;
};
// sizeof(Foo) == 16


Using a type that is explicitly 32 bits makes that cost clear.

A more subtle reason is that the C++ bool type is defined to only hold the values 0 or 1. Which means that when you assign to a bool, the compiler may have to emit code to enforce this. Consider bitflags:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
enum {
//...
  Flag_Baz = 1 << 3,
//...
};
// Here, compiler must do a compare against zero and then set the bool, rather than a direct assignment.
// baz_set will be either 0 or 1
bool baz_set = flags & Flag_Baz;

// Here, we can just assign the value directly:
// baz_set will be either 0 or Flag_Baz
b32 baz_set = flags & Flag_Baz;


Finally, if you're concerned about the memory cost, you shouldn't be using *either* b32 or bool. The standard bool still uses 8 bits to store 1 bit of data, which is an *awful* use of space. Instead we use implicit relationships to avoid storing boolean values, and bitflags to compress them together when we have to.
Jordan Duval
21 posts
What is the deal with Casey using bool32?
Hi Bryan,

Thanks for the reply.
That makes total sense and clears up my confusion. If you don't mind expanding a little bit more, what do you mean by implicit relationships?

Jordan
Bryan Taylor
55 posts
What is the deal with Casey using bool32?
jdduval
Hi Bryan,

Thanks for the reply.
That makes total sense and clears up my confusion. If you don't mind expanding a little bit more, what do you mean by implicit relationships?

Jordan

On reflection, "implicit relationship" is a pretty terrible wording. Lemme try again.

A boolean value is (almost always) used to dictate control flow. The goal here is to structure things so that the control flow you want happens without needing to check that value over and over again.

For a concrete example: let's suppose I have some update that I only want to apply to visible objects. I do a culling pass over all my objects and find that some of them are visible and some not. The naive approach is to store that state per-object and branch on it:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct FooObject {
    int x;
    int y;
    bool visible;
};

UpdateFoo(FooObject * foo) {
    if (foo->visible) {
        // ...
    }
}

// Alternatively applying SoA techniques:
struct FooObject {
    int x;
    int y;
};

// Storing the bools in an array at the very least gets us proper packing, 
// but it's still only 1/8 bits actually being used.
UpdateFoo(FooObject * foo, bool * visible, u32 count) {
    for (u32 i = 0; i < count; ++i) {
        if (visible[i]) {
            // ...
        }
    }
}


I can improve this by *not* storing the visible state directly, and implying it via a data structure. For example, I could store a list of visible object pointers:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct VisibleFooList {
    FooObject ** visible_foos;
    u32 count;
};

UpdateFoo(VisibleFooList * list) {
    for (u32 i = 0; i < list->count; ++i) {
        FooObject * foo = list->visible_foos[i];
        // ...
    }
}


You could also sort your primary storage based on that boolean state, etc. This is only one variation of this kind of strategy. Avoid storing boolean values when can cheaply recompute the predicate. These all have benefits above and beyond memory usage: less work done (because you don't look at things again to check their stored bool), fewer concerns about stale data.

Mike Acton's CppCon keynote talks about this quite a bit as well: https://www.youtube.com/watch?v=rX0ItVEVjHc
Casey Muratori
801 posts / 1 project
Casey Muratori is a programmer at Molly Rocket on the game 1935 and is the host of the educational programming series Handmade Hero.
What is the deal with Casey using bool32?
What Bryan said :)

To elaborate a little further, this is part of a larger a endemic problem to C that C++ made worse. There's been some attempts to fix it with the recent introduction of sized types (uint32_t, etc.), but unfortunately these are not defined by default on most compilers that I have seen.

Anyway, the problem is that C/C++ likes to talk about things in terms of what the language designers decided was a "good idea", but this has nothing to do with what the CPU actually does. You see this in a lot of places, like with "int", which I think (unless it's been changed) is defined such that you couldn't _ever_ really use it because you'd have no idea how high it can actually count :)

"bool" is just another one of these. It's defined so that programmers can have a conceptual tool for truth and falsehood, but because its specification does not match anything the CPU actually does in reality, it's not actually useful. So ironically, the type that means "bool" on a CPU is actually more logically "int"! Etc., etc.

So there's that, but then there's also the fact that if you are designing types that might be written to disk on one platform and read from disk on another where the types might not be the same size (eg., a 32-bit program writing and a 64-bit program reading), you typically need to be specific about the actual size things should be when you are doing I/O.

Put all this together, and it is not uncommon for a program to have _even more_ typedefs for bool - for example: b8, b16, b32, b64, bx.

In this case, we have the logical "bool" concept denoted by "b", which is just a semantic convention for the programmer so they can "say" that something is meant to be a true/false value, but then the full range of sizes from 8 to 64 so they can explicitly say how much space it should take up for storage. The "bx" type, on the other hand, is there to say "this is a boolean value and I don't care how much space it takes up", and that can be defined on the target platform to be whatever size would be most efficient - usually 32 or 64 bits, so it aligns naturally with the CPU's desired alignment.

Hope that helps clarify this - it's admittedly an odd subject area.

- Casey
Jordan Duval
21 posts
What is the deal with Casey using bool32?
Yep, cleared it up.

Thanks Bryan and Casey!