Layout of Rust's u128 and i128 changed
blog.rust-lang.org
blog.rust-lang.org
Also, in the 0x0x11223344556677889900aabbccddeeff sample value they're using, what is this 0x0x format? Is this some some low level asm thing, or is it just a formatting typo?
I found the LLVM/Clang bugs mentioned in the article kind of fascinating. As far as I know Clang has supported these types (partially) for quite a long time, so it's interesting that these issues weren't fixed until quite recently (if at all?).
I'm asking because I can't imagine writing a custom allocator against anything other than alignof(max_align_t).
Unless you're solely targeting <= C99… why use anything else?
Despite the alignof, int128 doesn't actually require 16-byte alignment on ARM64, so nothing goes wrong when you have an int128 within an 8-byte aligned struct.
Some x86-64 SIMD types do require 16-byte alignment (depending on what instructions you load them with). The Eigen math library, for instance, is slightly faster when you tell it to assume everything is 16-byte aligned, but you have to do some work to guarantee that's true. As well as calling aligned_alloc, you have to avoid locals since the stack is only 8-byte aligned.
Huh? Stack alignment is 16B ("The stack pointer on Apple platforms follows the ARM64 standard ABI and requires 16-byte alignment." https://developer.apple.com/documentation/xcode/writing-arm6...) and memory returned by the system malloc is always 16B aligned (it was 16B aligned even on 32b x86 and ARM).
> Unfortunately this meant some of the performance wins needed to be sacrificed to avoid an increased memory footprint.
https://doc.rust-lang.org/reference/type-layout.html#the-ali...
That would mean taking a reference of any field of that struct is undefined behavior, unless the compiler does some special magic for this specific case.
> Using `[repr(align(8))]` on the field
I don't think you can do that directly. You would need to use a newtype that specified the alignment, and use that type for the field.
Maybe some performance optimization hack where you can use a big integer instead of a float for faster math? Skip right past 64bit unix time and give yourself a lot of breathing room?
There are also some data formats that are 128 bit, like others mentioned.
Also, 2^64 is only about 10^19, so if you happen to have 20 exabytes that you want to byte-address, you can't do it with 64-bit pointers.
One week as I was preparing the report, it was clear something had gone haywire. Waste was roughly equal to total capacity. So either we'd lost all of our customers overnight, or there was a bug. Turns out the legacy billing system was using a long to count # of paid bytes and this had overflowed. So it does happen.
If the report runs and says something like "Fatal: Overflow while multiplying customer_space * repl_factor. Consider floating point numbers or a larger integer type" - you'd go "Oops" fix the bug and run it again.
In short, it was for fixed-point digital signal processing. The raw input and output samples were int64_t. We needed to add, subtract, multiply, and accumulate these to do filtering and linear regression with no loss of precision.
Conventional bigintegers weren't an option because the target application doesn't allow heap allocation. So we rolled our own [1] stack-allocated, fixed-width big integer class.
[1] https://github.com/the-aerospace-corporation/satcat5/blob/ma...
Also, if program uses lot of memory (due to alignment) it can cause swapping and the performance will be much worse than with unaligned storage.
For a lot of 128-bit arithmetic, it's also better to use the integer registers to take advantage of the ADC/ADCX/ADOX and MULX instructions for basic arithmetic operations. It actually depends a lot on the operation chain.
IMO it's actually a pretty big performance bug for 128-bit ints to not be 16-byte aligned.
This allows rust programs to actually use less memory than C programs.
It's possible to guarantee the layout of a struct by opting into a specific representation, such as the `repr(C)` shown in the OP.
> There is no indirection for these types; all data is stored within the struct, as you would expect in C. However with the exception of arrays (which are densely packed and in-order), the layout of data is not specified by default.
struct A {
a: i32,
b: u64,
}
struct B {
a: i32,
b: u64,
}
> Rust does guarantee that two instances of A have their data laid out in exactly the same way. However Rust does not currently guarantee that an instance of A has the same field ordering or padding as an instance of B.> By default, composite structures have an alignment equal to the maximum of their fields' alignments. Rust will consequently insert padding where necessary to ensure that all fields are properly aligned and that the overall type's size is a multiple of its alignment. [...]
> There is no indirection for these types; all data is stored within the struct, as you would expect in C. However with the exception of arrays (which are densely packed and in-order), the layout of data is not specified by default.
struct A {
a: i32,
b: u64,
}
struct B {
a: i32,
b: u64,
}
> Rust does guarantee that two instances of A have their data laid out in exactly the same way. However Rust does not currently guarantee that an instance of A has the same field ordering or padding as an instance of B.Here is my source: https://doc.rust-lang.org/nomicon/repr-rust.html
https://learn.microsoft.com/en-us/cpp/preprocessor/pack?view...
GCC and Clang also support this pragma:
https://gcc.gnu.org/onlinedocs/gcc-13.2.0/gcc/Structure-Layo...
https://clang.llvm.org/docs/UsersManual.html#microsoft-exten...
Rust can take it a bit further. In rust, fields must be aligned to a multiple of their size, and structs/tuples must be aligned to a multiple of their largest element. This combination makes it very easy to handle memory offsets, or reusing the same representation in a way that reduces memory usage (these are called niches)[1].
This is why dynamic linking is a bit more complex in rust. Alternatively you can offer a C bridge via `extern "c"`. C will act as a bridge between rust and different languages, and at that stage you need to take care of ordering and padding.
An unaligned access could be across 2 cache lines or 2 DDR Bursts. Or across a page table (I think 4096-bytes??) Or other higher level of organization requiring multiple accesses under the hood.
X86 does the easy thing and takes two or more clock ticks to read. But aligned accesses remain faster, likely for these low level groupings of bytes.
Some architectures straight up do not allow unaligned reads and force the programmer to read two registers and then extract the unaligned data.
-------
In particular, reading or writing across two cache lines can be disastrous loss of efficiencies. The memory system needs to lock / MESI indicate both cache blocks. If one (or the other) memory location is still locked by another thread / CPU core, you'll get false sharing.
As such, a common practice is to 64-byte / 512-bit align your memory accesses in heavily multithreaded code.
Also, reading a cache-line in shared MESI state should not cause false sharing degradation, only writing.
Also, you're correct that aside from caching effects, unaligned loads and stores generally carry no penalty on x86. This is one of the smarter (IMO) things that Intel/AMD have done to keep x86's market share.
That's largely obsolete for architectures that still get used now that individual transistors are cheap, since it turns out that it's much more efficient to do it in silicon, even if such accesses are pretty rare.
IIRC at least one compiler (maybe ICC?) no longer bothers to emit aligned instructions at all, even if it knows the data should be aligned, because they found it to be more trouble than it's worth on modern hardware.
(Still weird why would LLVM not align it to 16 bytes though)
[0] A Special Kind of Hell - intmax_t in C and C++: https://thephd.dev/intmax_t-hell-c++-c
1. the people at Intel who originally implemented `_BitInt()` made `_BitInt(128)` eight-byte aligned
2. the x86_64 psABI document was updated to agree with that
3. it was implemented in clang in such a way that it also made `_BitInt(128)` eight-byte aligned on arm64 ...
4. ... but the AAPCS says that it's sixteen-byte aligned
5. ... and maybe the x86_64 psABI is going to change to say that it's sixteen byte aligned after-all.
So--as of a month ago when I was studying this because we're in the middle of enabling `[U]Int128` in Swift--_BitInt(128) didn't agree with __int128[_t] on either platform, and that's definitely a bug on arm64, and, although it behaves as documented on x86_64, that also might change.LLVM had multiple ABI conformance bugs in its implementation of int128, but clang had workarounds for these bugs that rustc lacked, causing interop problems. I wonder why they put workarounds in clang instead of fixing LLVM?!
This is a great example of one of the criticisms of LLVM, that language front ends must still implement processor-specific details themselves – iirc another example is struct layout.
Among other things, because it would probably break rust (and other projects that had come to rely on the mistaken representation). Interoperability bugs like this often end up fundamentally unfixable in practice and the community ends up having to embrace bifurcated standards.
It is probably entirely necessary to do it in this particular situation, but when people argue for no software ever breaking backward compatibility, they're arguing for more and more convoluted solutions like this. There's always a cost.
Toolchains are poster children for that genre, yeah. The ecosystems are too large and too interconnected to evolve in parallel. I mean, any one of LLVM, Rust and Clang are too large as a single project to implement a breaking change like this as a single event, trying to get them to do it in tandem just isn't going to happen. And even then there are uncounted other LLVM downstreams to worry about, all of whom need a story for how to migrate.
I just want to get the people who scream on HN about how software should never ever break backcompat under any circumstance, in a room with the people on this issue who are stunned by how convoluted this situation is, and get them to fight it out. Bonus points for when those people are the same person on different days.
For example, I remember that for many years, Rust would have to throw away part of its aliasing analysis because its annotations were too precise and would confuse LLVM’s optimizer. And so on. But it’s getting better.