What the Hell Is a Target Triple?
80 points by bakaq
80 points by bakaq
They call little-endian MIPSes
mipsle
instead ofmipsel
.
Ah yes, British vs. American spelling.
This is the only place Go diverges from the Plan 9 naming. On plan 9, little endian mips is called ‘spim’. I think the Plan 9 name is much better.
Great article, now I’ve actually read it.
Why does ARM stick version numbers in the architecture name, instead of using -mcpu like you would on x86
The answer to this comes earlier. It’s because Arm was originally mostly a cross-compilation target in GCC. x86 hardware was much faster than most Arm devices (which were usually tiny handhelds vs big x86 desktops with 10-100x the power drain). This meant that you needed to be able to pick the right toolchain by using the target triple, rather than by picking the target triple and providing a load of flags. A lot of build systems were not set up properly for cross compiling, buy you could set CC
, CXX
, and LD
and they’d work, as long as you didn’t need to specify a load of other flags.
Clang has a similar feature. It looks at argv[0]
and checks whether it’s invoked as clang
, clang++
, or clang-cl
to define the driver behaviour, but also looks for a prefix to this as a target triple. This makes it easy to create a small family of symlinks for your tools and have them invoked correctly. Newer versions of clang, I believe, let this identify a config file rather than a triple, so you can add other flags.
Why the Windows people invented a whole other ABI instead of making things clean and simple like Apple did with Rosetta on ARM MacBooks?
Apple was doing a transition, Microsoft was not. When I left Microsoft, under 1% of total Windows installs were on Arm hardware. Their goal was to support x86-64 and AArch64 long term, whereas Apple’s was to deorbit the x86-64 bits as soon as possible and use AArch64 everywhere. The Windows team expects to be running new x86-64 binaries for decades. The EC ABI lets you build libraries that run on Arm hardware and are very fast to call from both Arm-native and emulated environments. That isn’t a goal for Apple: they want you to think of x86-64 as legacy and move on from it.
When we say “x86” unqualified, in 2025, we almost always mean x86_64, because 32-bit x86 is dead
I don’t agree here. x86-32 isn’t nearly as dead as I’d like, x86 means things that are common to 32- and 64-bit x86. Unless you’re on Windows, when x86 and x64 are distinct because the Windows team doesn’t know the difference between an ABI and an architecture.
amd64 refers to AMD’s original implementation of long mode in their K8 microarchitecture
As with arm64, this wasn’t AMD’s name. AMD’s first specs referred to it as x86-64. Other people called it amd64, just as Arm always calls their 64-bit architecture AArch64 but other people call it arm64.
This is likely complicated by the fact that there is also a x86_64-pc-windows-gnu triple, which is for MinGW code.
The Windows triples are even more complicated. There are three major ABIs supported in Clang: the MSVC-compatible one, the MinGW one and the Cygwin one. These all have some common features. The worst is MinGW, because it also has a bunch of sub-flavours with more or less overlap with the MSVC ABI. MinGW has an absolutely evil exception ABI that tries to layer Itanium-style personality functions on top of an SEH shim, but with some of the symbols that you need for interoperability between languages made private. Friends don’t let friends use MinGW.
For example, Apple targets never have this, because on an Apple platform, you just shut up and use CoreFoundation.framework as your libc
I believe you actually use libSystem, which links a load of other things.
Unfortunately, they decided to be different and special and do not use the target triple system for naming their targets
Oh, Go is so much more fun. They didn’t name their targets until quite recently, they numbered them. x86-64 is 6, x86-32 is 8, and so on. When I started writing go code, the instructions told you to run the compiler called 6g
.
That said, they inherited all of this from Plan 9. They didn’t, as the article implies ‘be clever by inventing a cute target triple convention’, Plan 9 created all of these conventions independently before target triples were a thing outside of one niche compiler that some weird MIT people liked.
And most imporantly: this sytem was built up organically. Disabuse yourself now of the idea that the system is consistent and that target triples are easy to parse. Trying to parse them will make you very sad
Importantly, triples are not really a thing. There isn’t (outside of rustc, maybe) a ‘list of triples that the compiler supports’. Think of each field in a triple is effectively a separate flag. A lot of logic is ‘if this OS and this architecture’, so they’re not independent flags.
As with arm64, this wasn’t AMD’s name. AMD’s first specs referred to it as x86-64. Other people called it amd64
I’m not sure that’s the whole story. I recently acquired one of the first gen 64-bit Opteron CPUs and it says “ᴀᴍᴅ64 technology for a simultaneous 32-bit and 64-bit computing environment” right on the front of the box: https://files.wezm.net/lobsters/amd64.jpg
aha, so ‘AMD64’ is the amd equivalent of ‘IA-32e’, whereas ‘x86_64’ is AMD’s ‘Intel® 64’. i get it!
The EC ABI lets you build libraries that run on Arm hardware and are very fast to call from both Arm-native and emulated environments. That isn’t a goal for Apple: they want you to think of x86-64 as legacy and move on from it.
macOS definitely allows emulated amd64 code to call arm64 libraries, it’s one of the things that helps Rosetta to go fast.
They didn’t name their targets until quite recently, they numbered them. x86-64 is 6, x86-32 is 8, and so on. When I started writing go code, the instructions told you to run the compiler called 6g.
They can be letters too: https://plan9.io/sys/doc/comp.html
Each CPU architecture supported by Plan 9 is identified by a single, arbitrary, alphanumeric character: k for SPARC, q for 32-bit Power PC, v for MIPS, 0 for little-endian MIPS, 5 for ARM v5 and later 32-bit architectures, 6 for AMD64, 8 for Intel 386, and 9 for 64-bit Power PC. The character labels the support tools and files for that architecture. For instance, for the 386 the compiler is 8c, the assembler is 8a, the link editor/loader is 8l, the object files are suffixed .8, and the default name for an executable file is 8.out.
macOS definitely allows emulated amd64 code to call arm64 libraries, it’s one of the things that helps Rosetta to go fast.
It does, but these trampolines require building a native call frame out of the emulated state. This is fine for a lot of the things Apple does. If you are calling, say, a font renderer then adding a dozen cycles to the call that takes tens of thousands to do useful work is fine. But if you’re calling smaller functions, that overhead can be high.
The EC ABI was designed so that the x86-64 emulator’s registers lined up with the native ones so you could just branch from an emulated x86-64 function to an AArch64 EC function with no bridging. The on-stack and in-register arguments were in the same places for both emulated x86-64 and native code. This made the bridging trivial.
The most important use case for this was incremental migration, where you’d ahead-of-time compile a handful of very hot functions for the EC ABI but keep most of them as emulated x86-64 routines, and gradually move them over. The hot routines are often the ones where the bridging overhead would be significant. You can also use the EC ABI for FX32-style local caching of compiled functions.
Huh, my impression was that The Most Important use case was basically running x86-64 plugins inside of native aarch64 hosts (like running translated VSTs in a native DAW). Is anyone actually doing the “EC library with hot code to fix perf on arm” thing?
That’s what the folks I spoke to on the Arm Windows team while they were developing this feature were doing for system DLLs.
And no, a “target quadruple” is not a thing and if I catch you saying that I’m gonna bonk you with an Intel optimization manual.
I’ve seen “target tuples” in some internal rustc docs which I think is a good name
Epic post!
The vendor … help[s] to sort related targets together.
So this is why I wasted so much time typing unknown
in the terminal? It now feels even worse :P
Oddly enough, Apple Clang uses arm64
for its arch field (e.g. arm64-apple-darwin24.3.0
) but standard clang uses aarch64
(e.g. aarch64-apple-darwin24.3.0
).
There’s some fun history there. Apple and Arm both worked on AArch64 back ends for LLVM while the architecture was not yet public. As soon as the embargo was lifted, they both contributed theirs upstream, which led to a situation where LLVM had both AArch64 (Arm-originated) and Arm64 (Apple-originated) targets in tree. And these needed target triple entries to disambiguate them. The Arm one had focused more on correctness, the Apple one on performance, so you typically wanted to use the Apple one if it worked or the Arm one if the Apple one miscompiled your code. These were gradually merged, but Apple was already using the arm64 target unconditionally by then.
I guess we should just count our lucky stars that we didn’t end up with an aarch64 target from Arm and an aarm64 target for Apple ARM…
I am also curious about more in-depth discussion of what actually is affected by the triples’s components.
Additionally, all four instruct you which version of libc to link to. And I imagine OS affects non-libc system libraries to link to?
I did my best to break this information down and try to get a SD-N document into the C++ standard both pre and during the pandemic with P1864. I did a LOT of research into the history of targets and this paper is kind of an explanation as to what goes where within a so-called triplet and how OS vs non-OS is affected. (e.g., UEFI environments vs custom environments/targets). Some of the contents might help answer your questions.
OS I think provides only default for object file format? That is, how do you actually write machine code to a file. Super-confused about this one.
For Rust the OS also selects standard library features
The C++ ABI is affected by the Fuchsia OS and the ARM (32-bit) architecture bits of the target triple. On Windows, it’s affected by pretty much every part of the triple.
Nice read!
…that’s specifically a GCC thing; it allows omitting parts of the triple in such a way that is unambiguous
Though “unambiguous” is perhaps a little generous. config.sub
is basically a huge lookup table aimed at resolving ambiguities but it’s not exactly intuitive.
When we say “x86” unqualified, in 2025, we almost always mean x86_64, because 32-bit x86 is dead. If you need to talk about 32-bit x86, you should either say “32-bit x86”, “protected mode”11, or “i386” (the first Intel microarchitecture that implemented protected mode)12. You should not call it x86_32 or just x86.
Maybe people “should” but nobody does unless it’s absolutely necessary to disambiguate? Because when someone says “x86” unqualified they mean either the 32-bit x86 processors or they’re talking generally about all x86 processors (and which is usually made clear from the context). Indeed Rust’s target_arch
configuration variable uses x86
to mean “any 32-bit x86 processor”. https://doc.rust-lang.org/reference/conditional-compilation.html#target_arch
IMHO, it would be more confusing to say “i386” when you don’t specifically mean the first Intel microarchitecture (although iirc some distros do confusingly have an “i386” target that can’t work on an i386 processor).
Great article but I was really put off by this bit, which aside from being very condescending, simply isn’t true and reveals a lack of appreciation for the innovation that I would have thought someone posting about target triples and compilers would have appreciated:
Why the Windows people invented a whole other ABI instead of making things clean and simple like Apple did with Rosetta on ARM MacBooks? I have no idea, but http://www.emulators.com/docs/abc_arm64ec_explained.htm contains various excuses, none of which I am impressed by. My read is that their compiler org was just worse at life than Apple’s, which is not surprising, since Apple does compilers better than anyone else in the business.
I was already familiar with ARM64EC from reading about its development from Microsoft over the past years but had not come across the emulators.com link before - it’s a stupendous (long) read and well worth the time if you are interested in lower-level shenanigans. The truth is that Microsoft’s ARM64EC solution is a hundred times more brilliant and a thousand times better for backwards (and forwards) compatibility than Rosetta on macOS, which gave the user a far inferior experience than native code, executed (sometimes far) slower, prevented interop between legacy and modern code, left app devs having to do a full port to move to use newer tech (or even just have a UI that matched the rest of the system), and was always intended as a merely transitional bit of tech to last the few years it took for native x86 apps to be developed and take the place (usurp) of old ppc ones.
Microsoft’s solution has none of these drawbacks (except the noted lack of AVX support), doesn’t require every app to be 2x or 3x as large as a sacrifice to the fat binaries hack, offers a much more elegant solution for developers to migrate their code (piecemeal or otherwise) to a new platform where they don’t know if it will be worth their time/money to invest in a full rewrite, lets users use all the apps they love, and maintains Microsoft’s very much well-earned legacy for backwards compatibility.
When you run an app for Windows 2000 on Windows 11 (x86 or ARM), you don’t see the old Windows 2000 aesthetic (and if you do, there’s an easy way for users to opt into newer theming rather than requiring the developer to do something about it) and you aren’t stuck with bugs from 30 years ago that were long since patched by the vendor many OS releases ago.
The truth is that Microsoft’s ARM64EC solution is a hundred times more brilliant and a thousand times better for backwards (and forwards) compatibility than Rosetta on macOS, which gave the user a far inferior experience than native code, executed (sometimes far) slower, prevented interop between legacy and modern code, left app devs having to do a full port to move to use newer tech (or even just have a UI that matched the rest of the system), and was always intended as a merely transitional bit of tech to last the few years it took for native x86 apps to be developed and take the place (usurp) of old ppc ones.
Since you mention PPC, does this criticism apply only to Rosetta, and not Rosetta2?
I believe it does. Rosetta 2 is lightyears beyond Rosetta when it comes to performance and emulation overhead strategies and benefits from hardware support (and having to do less work just because of fewer differences between the host/target architectures) but still fundamentally relies on the emulation the entirety of the stack. There is almost zero information about its internals disclosed, but from what I understand it still revolves around fat binaries - and necessitates that Apple compiles their frameworks against both x86_64 and arm64. Unlike the MS solution, with Rosetta 2 you cannot call a native ARM64 library from an x86_64 binary, you can’t port your code over piece-by-piece, and once Apple decides to no longer ship the next version of xxx framework as a fat binary because they don’t want to maintain support for two different architectures in their codebase (wholly understandable), you’ll (at best) be left with an older version of said framework that hasn’t been patched to address the latest bugs, doesn’t behave the same way that newer apps linking against the newer version of the framework do, etc.
Rosetta 2 does not require Fat Binaries. Fat Binaries (which Apple calls Universal binaries now) is the way you stop using Rosetta 2, that’s how you provide a natively-compiled app for both x86_64 and aarch64. Incidentally I’m actually a little surprised that Apple doesn’t do App Thinning on Universal apps through the Mac App Store (the way they do for iOS apps on the iOS App Store).
once Apple decides to no longer ship the next version of xxx framework as a fat binary because they don’t want to maintain support for two different architectures in their codebase (wholly understandable), you’ll (at best) be left with an older version of said framework that hasn’t been patched to address the latest bugs, doesn’t behave the same way that newer apps linking against the newer version of the framework do, etc
At such point as Apple decides to stop shipping OS frameworks as universal binaries, Rosetta 2 will break. There is no universe in which you get access to older versions of OS frameworks for Rosetta 2 apps. That said, the vast majority of framework code is presumably architecture-agnostic, and once the OS as a whole drops support for x86_64 then any new frameworks, or new symbols in existing frameworks, can be written as aarch64-only. And there is a universe where Apple stops shipping x86_64 code on the system but the Rosetta 2 support package includes the x86_64 frameworks (or AOT-translated versions of them). So that means Apple can choose to support Rosetta 2 for however long they want.
Yeah, figuring out what the allowed names for a triple are or what the target triple is that you need to compile something for a host in the cloud somewhere is hell. Great work everybody!