Layout
Every type has a size, the number of bytes one value occupies, and an alignment: a value of the type must sit at an address that is a multiple of it. The compiler fixes both for every type, and two operators report them.
size-query = "sizeof" "(" type ")"
alignment-query = "alignof" "(" type ")"
let bytes = sizeof(int64) * 4; // 32
let align = alignof(float32); // 4
sizeof and alignof
sizeof(T) and alignof(T) take a type, not a value, and produce a uint64 that is known at compile time. They can initialise a constant and appear in a when condition, and they are the way to compute an allocation's size: multiply a count by sizeof(T), never by a number written down.
const Record: uint64 = sizeof(int64) * 4;
func Main() -> int {
when sizeof(int) == 8 {
PrintLine("int is 64-bit on this target");
}
return 0;
}
A type whose layout is not known has no size — a flexible tail on its own, for one:
error: cannot determine the size of type 'uint8[]'
note: 'sizeof' needs a type whose layout is known at compile time
A constant computed from
sizeof or alignof is meant to be usable wherever a compile-time integer is required. rux 0.4.0 does not yet accept one as an array length or a repeat count: uint8[sizeof(int64)] fails with array length must be a non-negative compile-time integer. Write the length as a literal or a constant that does not use them.Primitive types
Every supported target is 64-bit, so these values are the same on all of them. Treat them as facts about the target, nonetheless, and ask sizeof and alignof rather than writing them into a program.
| Type | Size | Alignment |
|---|---|---|
bool8, int8, uint8, char8 | 1 | 1 |
bool16, int16, uint16, char16 | 2 | 2 |
bool32, int32, uint32, char32, float32 | 4 | 4 |
bool64, int64, uint64, char64, float64, int, uint | 8 | 8 |
int128, uint128 | 16 | 8 |
int256, uint256 | 32 | 8 |
int512, uint512 | 64 | 8 |
*T, *var T, &T, &var T, a function value | 8 | 8 |
() | 0 | 1 |
The aliases have the sizes of what they name: bool is 1, byte is 1, char is 4 and float is 8. See Primitive types.
Structs: natural alignment and padding
A struct keeps its fields in the order they are declared, and each field is placed at the next offset that is a multiple of its own alignment. Unused bytes left in between are padding. The struct's alignment is the largest alignment of its fields, and its size is rounded up to a multiple of that alignment, so that in an array every element is aligned too.
The same three fields therefore cost different amounts in different orders:
struct Loose {
flag: bool;
total: int64;
mark: uint8;
}
struct Tight {
total: int64;
flag: bool;
mark: uint8;
}
func Main() -> int {
PrintLine("Loose {} {}", sizeof(Loose), alignof(Loose));
PrintLine("Tight {} {}", sizeof(Tight), alignof(Tight));
var loose = Loose { flag: true, total: 1, mark: 2 };
let start = @loose as uint;
PrintLine("flag at {}, total at {}, mark at {}", (@loose.flag as uint) - start,
(@loose.total as uint) - start, (@loose.mark as uint) - start);
PrintLine("Loose[4] {}, Tight[4] {}", sizeof(Loose[4]), sizeof(Tight[4]));
return 0;
}
Loose 24 8
Tight 16 8
flag at 0, total at 8, mark at 16
Loose[4] 96, Tight[4] 64
flowchart TB
subgraph loose["Loose — 24 bytes"]
direction LR
l1["flag<br/>0"] --- l2["padding<br/>1–7"] --- l3["total<br/>8–15"] --- l4["mark<br/>16"] --- l5["padding<br/>17–23"]
end
subgraph tight["Tight — 16 bytes"]
direction LR
t1["total<br/>0–7"] --- t2["flag<br/>8"] --- t3["mark<br/>9"] --- t4["padding<br/>10–15"]
endThe compiler never reorders fields. Declaring the widest fields first usually leaves the least padding. A tuple is laid out the same way, its elements in order: (int8, int16, int8) is 6 bytes, aligned to 2.
Arrays
An array T[N] is its N elements side by side, each sizeof(T) bytes from the last, with nothing between them: sizeof(T[N]) is N * sizeof(T), and its alignment is T's. The padding inside each element is what keeps the next one aligned, so pointer arithmetic and indexing both step by sizeof(T).
A flexible tail T[], the last field of a struct, contributes its alignment and no storage. With struct Packet { length: uint16; bytes: uint32[]; }, sizeof(Packet) is 4 and alignof(Packet) is 4: the tail begins at offset 4, where the allocation continues. See Arrays.
Zero-sized types
(), an empty struct, and an aggregate made only of them occupy no bytes and have alignment 1. A zero-sized field takes no room in its struct: with struct Marker {}, a struct Tagged { id: int32; marker: Marker; flag: bool; } is 8 bytes, exactly as it would be without marker.
Views, ranges and interface values
| Type | Size | Alignment | Contents |
|---|---|---|---|
T[..], var T[..] | 16 | 8 | .data at 0, .length at 8 |
a..b, a..=b of a bound type B | 2 * sizeof(B) | alignof(B) | .start, then .end |
a.., ..b, ..=b | sizeof(B) | alignof(B) | the one bound |
.. | 0 | 1 | nothing |
| an interface value | 16 | 8 | two words: the data and its method table |
A slice's elements and an interface value's data live elsewhere and are not part of these sizes.
Optionals, fallibles and sums
Each level of an optional T?, a fallible T ! E and a sum A | B is a tagged aggregate: an 8-byte tag at offset 0, followed by the payload of the active case at the first offset after the tag that suits the payload's alignment. The size is that of the tag plus the largest payload, rounded up to the alignment. A zero-sized payload reserves nothing, so ()? is the tag alone.
| Type | Size | Alignment |
|---|---|---|
()? | 8 | 8 |
uint8?, int32?, int64? | 16 | 8 |
(*int)? | 16 | 8 |
int32?? | 24 | 8 |
int32 ! E, with E a struct of one int32 | 16 | 8 |
int32 | float64 | 16 | 8 |
Nested levels never share a tag: int32?? is an outer tag in front of a whole int32?. Absent is tag 0 and present tag 1; success is 0 and failure 1; a sum's members are numbered in their canonical order. These numbers describe what the compiler does today and are not a stable binary interface — never write a tag to a file or hand it to foreign code.
A variant is likewise a private tag followed by storage for its widest case, and an enum is exactly its base integer type: an enum Small: uint8 is 1 byte, an enum with no base type is 8.
Unions
A union overlays its members: every member starts at offset 0, and the union is as large as its largest member, rounded up to the largest alignment. It stores no tag, so writing one member and reading another reinterprets the same bytes:
union Bits {
whole: uint32,
bytes: uint8[4]
}
union Uneven {
small: uint8,
large: uint64
}
func Main() -> int {
PrintLine("{} {}", sizeof(Bits), sizeof(Uneven)); // 4 8
var bits = Bits { whole: 0x01020304u32 };
PrintLine("{}", bits.bytes[0]); // 4 on a little-endian target
return 0;
}
See also
- Arrays —
T[N]and flexible tails - Structs — field declarations
- Unions — overlapping storage
- Pointer arithmetic — offsets scaled by
sizeof - Foreign functions — layout at a C boundary
- Learn: Layout, Union, Raw memory