Calling all arguments

xania.org2025年12月16日 12:00

Written by me, proof-read by an LLM. Details at end.

Today we're looking at calling conventions - which aren't purely optimisation related but are important to understand. The calling convention is part of the ABI (Application Binary Interface), and varies from architecture to architecture and even OS to OS. Today I'll concentrate on the System V ABI for x86 on Linux, as (to me) it's the most sane ABI.1

Before we go on: I'd be remiss if I didn't point out that I can remember which register has what in it, and for years I had a Post It note on my monitor with a hand-written crib sheet of the ABI. While on holiday I had an idea: Why not put the ABI on a mug! I created these ABI mugs and you can get your own one - and support Compiler Explorer - at the .neverCompiler Explorer shop

The Compiler Explorer ABI mug: Get yours at the Some Compiler Explorer mugs with ABI information on themCE Shop!

We've already touched on calling conventions as I've commented the assembly in previous days, but concretely, for x86 Linux, the first couple of parameters go in and . This makes sense for discrete integer types, and even pointers. What about structures? Let's compare two functions:rdirsi2

With set to it might surprise you that the body of those two functions are identical! Both are just : We expect the separate arguments to be in and , but passing that larger structure by value got placed in and . Neat!ArgTypelonglea rax, [rdi+rsi]rdirsirdirsialso

If you explore by changing the you'll see that the compiler has to do more work if we don't (conveniently) use s here. For example, changing to changes the separate argument version to to reflect the 32-bit return value, but the body of the structure version becomes:ArgTypelongintlea eax...

It's a little tricky to follow as the compiler is cunningly switching between the 64-bit prefixed register names and the 32-bit versions, but you can see that, for the cost of a couple more instructions we still passed the structure pretty efficiently, and in a single register.re

It gets more interesting when we pass of arguments. Even on System V ABI, only the first 6 parameters are passed in registers. After that, it spills to the stack. Let's update our example to pass many arguments to show this:lots3

In this case we can see the separate args version adding all the registers, and then having to get some off the stack: and so on. On the structure side, the whole structure is copied to the stack, which seems bad.add rax, QWORD PTR [rsp+8]54

However! Changing to and you'll see an interesting difference. The separate args version is similar, though it can't use any more, and has to sign-extend all the values its getting from its registers. The struct args version has no stack spillage: our entire structure fits into one register! The compiler has a bit of a time shifting it around to extract each , but it's not having to spill to the stack at least. In my testing, different compilers used different tricks, so play around with the and compiler (e.g. clang) and get a sense of what it can do.ArgTypecharleaStructArgscharArgType6

So, all this is pretty abstract: why is this important? Sometimes knowing the ABI, and how the compiler can optimise around it can inform your design. There was some debate about the design of and and their usefulness: these are types that are convenient to pass , and so their footprint in registers is important.std::string_viewstd::optional87by value

Overall, knowing the calling convention helps you make smart decisions about layout and parameter passing that give the compiler the best shot at generating efficient code.

See that accompanies this post.the video

This post is day 16 of , a 25-day series exploring how compilers transform our code.Advent of Compiler Optimisations 2025

← | →AliasingInlining - the ultimate optimisation

This post was written by a human () and reviewed and proof-read by LLMs and humans.Matt Godbolt

.Support Compiler Explorer on or , or by buying CE products in the PatreonGitHubCompiler Explorer Shop

; structure is _packed_ into rdi as "y<<32 | x"movrax,rdi; rax = args.y<<32 | args.xshrrax,32; rax >>= 32; rax is now 'y'addeax,edi; y += x;ret

  1. Big thanks to for suggesting this topic and providing some fun examples. Jason Turner

  2. Or their smaller siblings /, / etc ediesidisi

  3. Which in general is a idea anyway. Don't pass lots of arguments, use structures and well defined types, not a load of bools and ints and whatnot. bad

  4. Though the compiler does a decent job of using vector operations here. Which I promise we'll talk about eventually! 

  5. Remember we're passing by here, and typically you might normally pass by const reference. Experiment with the code and see the difference it makes. value

  6. If you get confused, pop out the view with the "Edit on Compiler Explorer" button and consider trying the Claude Explain AI explanation panel from the "Add new..." drop down. 

  7. A is essentially a pointer and a length - two values that fit neatly into two registers, just like the two- struct example above. std::string_viewlong

  8. The Windows ABI in particular only has four register arguments and so only two s can be passed before things end up on the stack. string_view