Skip to content

Instantly share code, notes, and snippets.

@s4hubhamp
Created April 2, 2025 10:08
Show Gist options
  • Save s4hubhamp/89e126a64ff9191b11ef16a7a8a747c4 to your computer and use it in GitHub Desktop.
Save s4hubhamp/89e126a64ff9191b11ef16a7a8a747c4 to your computer and use it in GitHub Desktop.
Alignments in Zig
pub fn main() !void {
// Alignment in computer programming refers to the way data is arranged and accessed in memory.
// It is a rule that specifies the memory address boundaries at which a data type or variable
// should be stored.
// The goal of alignment is to ensure that memory accesses are efficient and that the processor
// can retrieve data in the most optimized way.
// https://ziggit.dev/t/whats-up-with-alignof-u128/1271/3?u=s4hubhamp
// Definition as per Zig docs
// Each type has an alignment - a number of bytes such that, when a value of the type is loaded from or stored to memory,
// the memory address must be evenly divisible by this number. You can use @alignOf to find out this value for any type.
// Alignment depends on the CPU architecture, *but is always a power of two*, and less than 1 << 29.
//
// cpu always reads one word at a time. On 64 bit processor word size is 64 bits and on 32 bit it's 32 bits.
// What this means is that we will be accessing memory locations with 8 byte boundaries.
// For example, in first cycle we read 0 - 7 and in next cycle we read 8 - 15.
// So, It's helpful to arrange the data such that it's optimal to do load or store operations.
//
// Let's say we have four variables
// u32, u16, u8, bool and natural alignment for all the fields is 4, 2, 1, 1 bytes respectively.
// let's say we start from memory address 0.
// u32 which requires 4 bytes to store the data can be placed at 0, 4, 8, 12
// u16 can be placed at 0, 2, 4, 6
// u8 can be placed anywhere
//
// Why we can't place u32 at addresses 6 - 9?
// In one cycle cpu will read first 0 - 8 bytes and in next it will read 9 - 15
// In order to access our u32 cpu needs to do two cycles. And this is inefficient.
//
// If we can read 8 bytes at a time why can't we place u32 at addresses 1 to 4 (anyway we are loading 0 - 7 in one cycle)?
// CPU also expects the start addresses to be multiple of types alignment. When you read a 32-bit value (u32),
// the processor expects the starting address to be aligned to a 4-byte boundary (e.g., address 0, 4, 8, 12).
// This is because we will have predictability and processors are designed to handle data aligned in a certain way
//
// Types size is always multiple of it's alignment.
//
// The memory address where a variable or object is stored must be a multiple of the type's alignment.
// For example:
// If a type has an alignment of 4 bytes, it should be placed at an address that is divisible by 4 (e.g., 0x04, 0x08, 0x0C, etc.).
// For 8-byte alignment, the memory address should be a multiple of 8 (e.g., 0x08, 0x10, 0x18).
// structs alignment is the largest alignment between all struct members.
//
// What is the usecase for arbitrary width types if sizes are getting round up to next size which is power of two?
// Sizes change in context of packed structs and there are sevaral other use cases
// visit https://ziggit.dev/t/understanding-arbitrary-bit-width-integers/2028/1
{
std.debug.print("\tAlignment of Primitives\n", .{});
std.debug.print("\tSize of u8: {}\talignment of u8: {}\n", .{ @sizeOf(u8), @alignOf(u8) }); // 1 1
std.debug.print("\tSize of u9: {}\talignment of u9: {}\n", .{ @sizeOf(u9), @alignOf(u9) }); // 2 2
std.debug.print("\tSize of u16: {}\talignment of u16: {}\n", .{ @sizeOf(u16), @alignOf(u16) }); // 2 2
std.debug.print("\tSize of u20: {}\talignment of u20: {}\n", .{ @sizeOf(u20), @alignOf(u20) }); // 4 4
std.debug.print("\tSize of u32: {}\talignment of u32: {}\n", .{ @sizeOf(u32), @alignOf(u32) }); // 4 4
std.debug.print("\tSize of u64: {}\talignment of u64: {}\n", .{ @sizeOf(u64), @alignOf(u64) }); // 8 8
// https://ziggit.dev/t/whats-up-with-alignof-u128/1271/3
std.debug.print("\tSize of u65: {}\talignment of u65: {}\n", .{ @sizeOf(u65), @alignOf(u65) }); // 16 16
std.debug.print("\tSize of u128: {}\talignment of u128: {}\n", .{ @sizeOf(u128), @alignOf(u128) }); // 16 16
std.debug.print("\tSize of u256: {}\talignment of u256: {}\n", .{ @sizeOf(u256), @alignOf(u256) }); // 32 16
std.debug.print("\tSize of u512: {}\talignment of u512: {}\n", .{ @sizeOf(u512), @alignOf(u512) }); // 64 16
// why For u256 and u512 Zig chose the alignment to be 16 despite the size was bigger?
// Zig chooses 16-byte alignment for types like u256 and u512 because it strikes a balance between the size of the type
// and optimal memory access performance. Even though the size of these types is larger than 16 bytes,
// 16-byte alignment helps with CPU efficiency, cache usage, and SIMD optimizations.
std.debug.print("\n\tTaking a closer look at array of u256\n", .{});
const array = [2]u256{ 1, 2 };
std.debug.print("\tarray[0]: {} array[1]: {} offset of array[1] is: {}\n", .{
@intFromPtr(&array[0]),
@intFromPtr(&array[1]),
@intFromPtr(&array[1]) - @intFromPtr(&array[0]),
});
std.debug.print("\tSize of [2]u256: {}\n\n", .{@sizeOf([2]u256)});
}
{
std.debug.print("\tAlignment of Normal structs\n", .{});
const S = struct {
a: bool = true,
b: bool = false,
c: u8 = 1,
d: u32 = 1,
e: u64 = 1,
};
// structs alignment is equal to largest alignment from it's members, alignment of u64 = 8
std.debug.print("\tstructs alignment: {}\n", .{@alignOf(S)});
//
// So the structs alignment is 8 this means that we avoid 1 member from spanning across
// some bytes before 8 and some bytesafter 8.
//
// |________________ _________________ ______________________________________________________|
// 0 7 8 11 12 15
// - u64(8 bytes) - - u32(4 bytes) - - u8(1 byte) + bool * 2(2 bytes) + 1 byte of padding -
// 0 -> 7 8 -> 11 12 -> 15
// Tatal size -- 16 bytes
//
std.debug.print("\tstructs size: {}\n", .{@sizeOf(S)});
print_struct_info(S);
}
{
std.debug.print("\tAlignment of Extern structs\n", .{});
// An extern struct has in-memory layout matching the C ABI for the target.
// If well-defined in-memory layout is not required, struct is a better choice
// because it places fewer restrictions on the compiler.
// In case of normal struct Zig rearranged the members in order to utilize maximum space
// but in case of extern struct zig won't rearrange the fields.
const S = extern struct {
a: bool = true,
b: bool = false,
c: u8 = 1,
d: u32 = 1,
e: u64 = 1,
};
// structs alignment is equal to largest alignment from it's members, alignment of u64 = 8
std.debug.print("\tstructs alignment: {}\n", .{@alignOf(S)});
//
// So the structs alignment is 8 that means the fields are stored such that they are 8 bytes aligned
// what does this mean? This means that we ensure that we don't span addresses that starts within 8 byte boundary and
// go outside of that boundary
//
// |_____________________________________________________ _________________ _________________|
// 0 3 4 7 8 15
// - bool * 2(2 bytes) + u8(1 byte) + 1 byte of padding - - u32(4 bytes) - - u64(8 bytes) -
// 0 -> 3 4 -> 7 8 -> 15
// Tatal size -- 16 bytes
//
std.debug.print("\tstructs size: {}\n", .{@sizeOf(S)});
print_struct_info(S);
}
{
// TODO add usescases
std.debug.print("\tCustom alignment for each struct member\n", .{});
// The alignment ensures that the field is placed at an address that is a multiple of the specified alignment
// Below `a` has alignment of 64 but it takes only 4 bytes. b's offset is 4.
const S = struct {
a: u32 align(64) = 1,
b: u32 align(2) = 1,
};
// structs alignment is equal to largest alignment = 64
std.debug.print("\tstructs alignment: {}\n", .{@alignOf(S)});
std.debug.print("\tstructs size: {}\n", .{@sizeOf(S)});
print_struct_info(S);
// https://github.com/ziglang/zig/issues/1512
}
{
// TODO add use cases
std.debug.print("\tAlignment for packed structs\n", .{});
// struct fields have total size of 160 but since the alignment is 16, the padding is automatically added at the end
const S = packed struct(u160) {
account_number: u31 = 1,
is_active: bool = true, // we can use u1 to store boolean 1 or 0. In packed struct by default bool uses only 1 bit
balance: u128 = 1,
};
// packed structs alignment is equal to alignment of backing integer so it is @alignOf(u160) = 16
std.debug.print("\tstructs alignment: {}\n", .{@alignOf(S)});
//
// |_____________________ ____________________________ ________________________|
// 0 15 16 19 20 31
// - u128 (16 bytes) - - (u31 + bool) (4 bytes) - - PADDING (12 bytes) -
// Tatal size -- 32 bytes
//
std.debug.print("\tstructs size: {}\n", .{@sizeOf(S)});
print_struct_info(S);
}
}
fn print_struct_info(S: type) void {
const info = @typeInfo(S);
const s = S{};
inline for (info.@"struct".fields) |field| {
std.debug.print("\t\tfield: {s}\n", .{field.name});
std.debug.print("\t\tsize: {}\n", .{@sizeOf(field.type)});
std.debug.print("\t\toffset: {}\n", .{@offsetOf(S, field.name)});
std.debug.print("\t\talignment: {}\n", .{field.alignment});
std.debug.print("\t\taddress: {*}\n", .{&@field(s, field.name)});
std.debug.print("\n", .{});
}
std.debug.print("\n", .{});
}
const std = @import("std");
@s4hubhamp
Copy link
Author

For better example of extern case watch https://www.youtube.com/watch?v=mJs2E7mkhx4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment