· 5 min read ·

The Endianness Portability Trap: When Abstraction Costs More Than It Buys

Source: lobsters

Endianness debates feel like they belong to a prior era of computing, when SPARC workstations and PowerPC servers were real targets you had to think about. This piece on endian wars and anti-portability brings the argument back into focus: sometimes writing endian-neutral code is the wrong call, and pretending otherwise creates real costs.

That position deserves a closer look, because the landscape has changed substantially.

Where Big-Endian Actually Lives Now

Let’s start with the hardware reality. x86-64 is little-endian. ARM, which now runs everything from phones to Apple’s laptops to AWS Graviton servers, ships in little-endian mode almost universally, even though the architecture technically supports both. RISC-V defaults to little-endian. The big-endian holdouts are SPARC (barely alive outside Oracle legacy contracts), IBM Z/Architecture mainframes, and certain MIPS and PowerPC embedded targets.

If you write a general-purpose application or a library today and obsessively abstract over endianness, the realistic probability that someone runs it on a big-endian machine approaches zero for most use cases. The abstraction is real work, producing real complexity, guarding against a scenario that mostly does not happen.

This is what “anti-portability” means in this context: not abandoning portability as a value, but recognizing that portability has costs, and those costs should be proportional to the actual benefit. Blind portability theater serves no one.

The Cost Is Not Just Verbosity

The usual counter-argument is that endian-safe code is cheap: a few macros, a byteswap function, done. In practice, it rarely stays that cheap.

Consider what a byteswap abstraction layer actually involves in a real codebase. You need to decide whether your on-disk format is defined as little-endian or big-endian. Then every read and write to that format needs a conversion call. If you use typed wrappers, you need those wrappers to be zero-overhead across compilers. You need to make sure the optimizer actually eliminates the no-op path on the native platform, which it usually does, but not always.

More insidiously, endian abstraction tends to spread. Once you’ve committed to it, every struct that crosses a process boundary, every serialized message, every memory-mapped file becomes something that the team second-guesses. Is this field being swapped? Should it be? The cognitive overhead of tracking which values are in which byte order is non-trivial, and bugs here are the silent, data-corrupting kind.

The Linux kernel’s approach is instructive. It uses annotated types, __le32 and __be16 and so on, combined with sparse’s __attribute__((bitwise)) checking, to make byte-order errors detectable at compile time:

__le32 on_disk_value = cpu_to_le32(host_value);
__be16 network_port  = cpu_to_be16(port_number);

This is the right design: make the byte order explicit in the type, not an afterthought in the function call. But it requires buy-in from the whole codebase and a checker like sparse to enforce it. Without that infrastructure, you just have uint32_t everywhere and hope.

What C++20 Finally Acknowledged

For a long time, C and C++ had no portable way to even query the endianness of the target platform at compile time. You had __BYTE_ORDER__ as a GCC extension, _WIN32 heuristics, POSIX’s <endian.h> which wasn’t available on all platforms, and a small industry of autoconf checks.

C++20 introduced std::endian in <bit>, finally giving the language a portable compile-time constant:

#include <bit>

if constexpr (std::endian::native == std::endian::little) {
    // fast path: data is already little-endian
} else {
    // need to byteswap
}

C++23 added std::byteswap as a standard way to reverse byte order, compiling down to a single bswap instruction on x86 or rev on ARM. These are welcome additions, but their arrival in 2020 and 2023 respectively is telling: for forty-plus years of C++ history, the standard had nothing to say about byte order at all.

Rust Gets the Model Right

Rust’s approach is worth studying because it enforces the right habit without requiring a separate checker. The standard library provides explicit methods on primitive integers:

let value: u32 = 0x01020304;
let le_bytes = value.to_le_bytes();  // [0x04, 0x03, 0x02, 0x01]
let be_bytes = value.to_be_bytes();  // [0x01, 0x02, 0x03, 0x04]

// Reading from a buffer:
let from_le = u32::from_le_bytes([0x04, 0x03, 0x02, 0x01]);
let from_be = u32::from_be_bytes([0x01, 0x02, 0x03, 0x04]);

There is no to_native_bytes. The choice of byte order is forced at the call site. When you write a file parser, you choose from_le_bytes or from_be_bytes based on what the format specifies, and the decision is visible and searchable. The byteorder crate extends this to I/O streams with the same philosophy.

This design sidesteps the portability question almost entirely: you never swap based on the host platform, you always read and write a known byte order. The no-op on a matching-endian host is still there in the generated code, but LLVM eliminates it reliably. You get correctness without the abstraction layer.

When Portability Actually Matters

None of this means endian-neutral code is never worth writing. The calculus changes when you’re building something that genuinely has to run on heterogeneous hardware.

Network protocols are the canonical case. TCP/IP chose big-endian as the network byte order, and htons/ntohl and their variants exist precisely because you cannot know what byte order the other end is using. A packet sent from an x86 box and received on a hypothetical big-endian host needs the fields in a defined order, and that order has to be specified somewhere.

File formats for long-lived interchange have the same constraint. JPEG uses big-endian for its EXIF metadata. WAV and RIFF are little-endian. TIFF supports both and embeds a byte-order marker, which is elegant in theory and a source of parser complexity in practice. If you are defining a new binary format that you expect to remain readable in thirty years on hardware you cannot predict, specifying a byte order and implementing the conversion honestly is the right call.

Embedded systems are different again. If you control both the producer and consumer, and they run on the same microcontroller family, mandating little-endian in your internal protocol and skipping the conversion layer is a completely reasonable optimization. The byteswap instruction is cheap, but it is not free, and on a constrained processor doing it thousands of times per second matters.

The Actual Argument for Anti-Portability

The argument is not that endianness is unimportant. It is that the correct response to endianness is to make a deliberate choice, document it, and enforce it, rather than writing code that tries to be neutral about something that has a defined answer.

Choosing little-endian for your internal binary format in 2026 is not reckless. It is a defensible engineering decision given that every mainstream deployment target runs little-endian. Choosing to skip the conversion layer in code that will never leave x86-64 and ARM64 is an acceptable trade-off. The portability you would be sacrificing is portability to hardware that represents a rounding error in your actual deployment profile.

What is not acceptable is writing the conversion layer carelessly, mixing endian-aware and endian-naive code in the same format, or leaving the byte order of your on-disk structures undocumented. The discipline endianness requires is not neutrality, it is explicitness. Pick a side, write it down, and enforce it in the type system where you can.

The platforms that forced the old portability calculus are mostly gone. The code habits they produced are still here, quietly adding complexity to codebases that will never see a SPARC.

Was this interesting?