# Understanding arbitrary bit-width integers

**URL:** <https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028>\
**Category:** Help\
**Tags:** memory-management\
**Created:** [October 24, 2023, 3:44pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028 "2023-10-24T15:44:55Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![FObersteiner](https://ziggit.dev/user_avatar/ziggit.dev/fobersteiner/32/1177_2.png) [@FObersteiner](https://ziggit.dev/u/FObersteiner)\
**Post date:** [October 24, 2023, 3:44pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/1 "2023-10-24T15:44:55Z")

</div>

The zig language supports [arbitrary bit-width for integers](https://ziglang.org/documentation/master/#Primitive-Types). I see that this can be useful for instance to specify the boundaries of a parameter: e.g. i7 =\> this only goes from -64 to 63.

However, besides having a “more precise typing system”, what’s the purpose / application? To my understanding, an i7 will still occupy a full byte in memory ([packed structs](https://ziglang.org/documentation/master/#packed-struct) seem to be an exception here)? Does it for example help in arithmetic operations (performance-wise) to know there are only so many bits to consider?

p.s. I just started learning zig for fun, and normally, I don’t dabble on the bit-level 😉

---

<div class="post-metadata">

**Author:** ![IntegratedQuantum](https://ziggit.dev/user_avatar/ziggit.dev/integratedquantum/32/782_2.png) [@IntegratedQuantum](https://ziggit.dev/u/IntegratedQuantum)\
**Post date:** [October 24, 2023, 5:40pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/2 "2023-10-24T17:40:18Z")

</div>

I use `u31` pretty often because it can coerce to an `i32` as well as a `usize` without requiring explicit casting.

Apart from that compiler could theroretically do more optimizations on them. For example a `?u31` or `!u31` can be put into 4 bytes, whereas a `?u32` or `!u32` requires 8 bytes.  
But that optimization isn’t implemented yet.

And then I think it’s also useful when you need big numbers. I have used `u128` a couple of times when `u64` was too small.

Additionally low-bit numbers are sometimes useful for their overflow behavior

```zig
var inu8: u8 = ...;
inu8 = (inu8 + 1)%16;
var inu4: u4 = ...;
inu4 +%= 1; // This is much simpler to use.

```

---

<div class="post-metadata">

**Author:** ![matklad](https://ziggit.dev/user_avatar/ziggit.dev/matklad/32/2462_2.png) [@matklad](https://ziggit.dev/u/matklad)\
**Post date:** [October 24, 2023, 5:52pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/3 "2023-10-24T17:52:20Z")

</div>

Couple of things we use oddly-sized integers at TigeBeetle:

- Packed structs:  
[tigerbeetle/src/lsm/schema.zig at 216c196cd5eb27f36449b5c22889658ce512bc70 · tigerbeetle/tigerbeetle · GitHub](https://github.com/tigerbeetle/tigerbeetle/blob/216c196cd5eb27f36449b5c22889658ce512bc70/src/lsm/schema.zig#L440-L447)

- Non-zero signed integers

- Bit flags:

---

<div class="post-metadata">

**Author:** ![dee0xeed](https://ziggit.dev/letter_avatar_proxy/v4/letter/d/3ab097/32.png) [@dee0xeed](https://ziggit.dev/u/dee0xeed)\
**Post date:** [October 24, 2023, 6:10pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/4 "2023-10-24T18:10:35Z")

</div>

Sometimes you have to use integers with bit length \< 8 and this is forced by the compiler.  
Consider this code:

```zig
    pub fn init(a: Allocator, ctx_len: u5) !BitPredictor {
        var bp = BitPredictor{};
        bp.p0 = try a.alloc(u16, @as(u32, 1) << ctx_len);
        @memset(bp.p0, P0MAX / 2);
        return bp;
    }

```

Any type of `ctx_len` longer than `u5` can potentially result in overflow, so the compiler performs some smart checks. Let’s try use `u6` instead of `u5`. We’ll get this error:

```plaintext
src/bit-predictor.zig:17:49: error: expected type 'u5', found 'u6'
        bp.p0 = try a.alloc(u16, @as(u32, 1) << ctx_len);
                                                ^ ~~~~~~
src/bit-predictor.zig:17:49: note: unsigned 5-bit int cannot represent all possible unsigned 6-bit values

```

Compiler deduced that for shifting `u32` 1 to the left we can use `u5` as a maximum and you have to use `u5` (or shorter integers).

---

<div class="post-metadata">

**Author:** ![FObersteiner](https://ziggit.dev/user_avatar/ziggit.dev/fobersteiner/32/1177_2.png) [@FObersteiner](https://ziggit.dev/u/FObersteiner)\
**Post date:** [October 26, 2023, 4:30pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/5 "2023-10-26T16:30:07Z")

</div>

Thanks a lot for your replies and examples! I didn’t actively code in a language before that gave me so much control - still getting used to it. More control means more responsibility I guess 😉

---

<div class="post-metadata">

**Author:** ![Validark](https://ziggit.dev/user_avatar/ziggit.dev/validark/32/646_2.png) [@Validark](https://ziggit.dev/u/Validark)\
**Post date:** [October 27, 2023, 1:00pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/6 "2023-10-27T13:00:16Z")

</div>

Non-zero or non-negative?

---

<div class="post-metadata">

**Author:** ![chung-leong](https://ziggit.dev/user_avatar/ziggit.dev/chung-leong/32/999_2.png) [@chung-leong](https://ziggit.dev/u/chung-leong)\
**Post date:** [October 27, 2023, 7:34pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/7 "2023-10-27T19:34:19Z")

</div>

Here’s a neat trick you can pull at comptime with the help of arbitrary-width integer:

Suppose you want to assign a number to a set of functions based on their signatures. Functions with different arguments or return values would get different numbers. Functions with the same arguments and return values would get the same number.

First, the sample input:

```zig
const ns = struct {
    fn apple(arg1: u32, arg2: u32) void {
        _ = arg1;
        _ = arg2;
    }

    fn orange(arg1: u32, arg2: u32) u32 {
        return arg1 + arg2;
    }

    fn banana(arg1: u32, arg2: u32) void {
        _ = arg1;
        _ = arg2;
    }
};

```

As you can see, apple and banana have the same signature. If you run this code:

```zig
std.debug.print("apple: {s}\n", .{@typeName(@TypeOf(ns.apple))});
std.debug.print("orange: {s}\n", .{@typeName(@TypeOf(ns.orange))});
std.debug.print("banana: {s}\n", .{@typeName(@TypeOf(ns.banana))});

```

you would get:

```zig
apple: fn(u32, u32) void
orange: fn(u32, u32) u32
banana: fn(u32, u32) void

```

Now, the code for the counter:

```zig
const counter = create: {
    comptime var next = 0;

    break :create struct {
        fn get(comptime anything: anytype) comptime_int {
            _ = anything;
            const slot = next;
            next += 1;
            return slot;
        }
    };
};

```

Due to comptime memoization, `counter.get()` will only increment the counter if the argument given is something it hasn’t seen before. Since @typeName() return the same text string for apple and banana, you should get the same number, right?

```zig
const apple_slot = counter.get(@typeName(@TypeOf(ns.apple)));
const orange_slot = counter.get(@typeName(@TypeOf(ns.orange)));
const banana_slot = counter.get(@typeName(@TypeOf(ns.banana)));
std.debug.print("{d} {d} {d}\n", .{ apple_slot, orange_slot, banana_slot });

```

Output:

```zig
0 1 2

```

Nope. This is because strings are fat-pointers. Two identical strings stored at different memory location will be considered different by Zig. Here’s where arbitrary bit-with integer comes in. By convert strings into giant integers, we can force Zig to compare at comptime the actual data that the pointers point to:

```zig
fn signature(comptime f: anytype) comptime_int {
    const name = @typeName(@TypeOf(f));
    comptime var int = 0;
    inline for (name) |c| {
        int = (int << 8) | @as(comptime_int, @intCast(c));
    }
    return int;
}

const apple_slot = counter.get(signature(ns.apple));
const orange_slot = counter.get(signature(ns.orange));
const banana_slot = counter.get(signature(ns.banana));
std.debug.print("\n{d} {d} {d}\n", .{ apple_slot, orange_slot, banana_slot });

```

Result:

```zig
0 1 0

```

---

<div class="post-metadata">

**Author:** ![dude\_the\_builder](https://ziggit.dev/user_avatar/ziggit.dev/dude_the_builder/32/557_2.png) [@dude\_the\_builder](https://ziggit.dev/u/dude_the_builder)\
**Post date:** [October 27, 2023, 9:44pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/8 "2023-10-27T21:44:27Z")

</div>

Mind officially blown! 😮 Thanks for sharing this.

---

<div class="post-metadata">

**Author:** ![permutationlock](https://ziggit.dev/user_avatar/ziggit.dev/permutationlock/32/9958_2.png) [@permutationlock](https://ziggit.dev/u/permutationlock)\
**Post date:** [October 27, 2023, 9:55pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/9 "2023-10-27T21:55:51Z")

</div>

You can also get this functionality by using the type itself unless I am missing something.

```zig
const apple_slot = counter.get(@TypeOf(ns.apple));
const orange_slot = counter.get(@TypeOf(ns.orange));
const banana_slot = counter.get(@TypeOf(ns.banana));
std.debug.print("{d} {d} {d}\n", .{ apple_slot, orange_slot, banana_slot });

```

```zig
0 1 0

```

---

<div class="post-metadata">

**Author:** ![chung-leong](https://ziggit.dev/user_avatar/ziggit.dev/chung-leong/32/999_2.png) [@chung-leong](https://ziggit.dev/u/chung-leong)\
**Post date:** [October 28, 2023, 8:14am UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/10 "2023-10-28T08:14:33Z")

</div>

Curious. I didn’t realize that @typeName() will give you a different pointer even when the input is the same.

```zig
    std.debug.print("{d} {d}\n", .{ @intFromPtr(@typeName(@TypeOf(ns.apple)).ptr), @intFromPtr(@typeName(@TypeOf(ns.apple)).ptr) });

```

```zig
2164496 2164514

```

In any event, in the original code I was using sub-strings of the @typeName() as key. The code above was simplified from that.

---

<div class="post-metadata">

**Author:** ![FObersteiner](https://ziggit.dev/user_avatar/ziggit.dev/fobersteiner/32/1177_2.png) [@FObersteiner](https://ziggit.dev/u/FObersteiner)\
**Post date:** [April 10, 2024, 10:45am UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/11 "2024-04-10T10:45:19Z")

</div>

I think this is worth linking here @cancername - great example why at the moment, arbitrary bit width comes with a downside (performance penalty): LLVM seems “confused”

> **[Compiler Explorer - Zig (zig trunk)](https://godbolt.org/z/MTrvzz4nd)**
>
> const std = @import("std");
> 
> const Sussy = struct {
> x: u31,
> y: u4,
> z: u62,
> };
> 
> // Since overflow can't happen with the \`+\` operator,
> // the compiler should optimize this to be equivalent
> // to \`unsussy\`. However, since LLVM doesn't...

Issue on github: [LLVM: Non power of two integer arithmetic emits slow assembly · Issue #19616 · ziglang/zig · GitHub](https://github.com/ziglang/zig/issues/19616)

---

<div class="post-metadata">

**Author:** ![nyc](https://ziggit.dev/user_avatar/ziggit.dev/nyc/32/1167_2.png) [@nyc](https://ziggit.dev/u/nyc)\
**Post date:** [April 11, 2024, 7:00pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/12 "2024-04-11T19:00:39Z")

</div>

> [@dee0xeed](#):
>
> so the compiler performs some smart checks

I wouldn’t call them smart. I hit this too often where I know the value and the compiler tries to be smart but I just wind up with casts all over the place to placate it.

---

<div class="post-metadata">

**Author:** ![nyc](https://ziggit.dev/user_avatar/ziggit.dev/nyc/32/1167_2.png) [@nyc](https://ziggit.dev/u/nyc)\
**Post date:** [April 11, 2024, 7:06pm UTC](https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/13 "2024-04-11T19:06:00Z")

</div>

How do you get compiler enforced non-zero numbers with bit widths?
