-
-
Save s4hubhamp/89e126a64ff9191b11ef16a7a8a747c4 to your computer and use it in GitHub Desktop.
Alignments in Zig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For better example of extern case watch https://www.youtube.com/watch?v=mJs2E7mkhx4