138 comments

[ 3.0 ms ] story [ 234 ms ] thread
I'm reminded of Chesteron's Fence in this.

Every major ABI is listed here as containing the same mistakes. I'm inclined to think the people who designed these ABIs were smart enough to understand the consequences of their design decisions.

I don't know whether this author is correct or not, but my gut is there is something missing here with respect to non local control flow (like exception handling, setjmp/longjmp, and fibers).

I love seeing others bring up Chesterton's fence; it's been a reference that comes to mind with quite a lot of the WTFery I've encountered in my career (usually it remains WTFery even when looking for underlying reasons, but it at least helps remind me to question my instincts).

I don't really know enough to weigh in on this, but I can say that having pursued a lot of WTFish things in my career so far, 90% of the times I've encountered bad decisions, the explanation for it was either "it was done that way because legacy reasons" (i.e., it had to be done that way then, the reason it had to be has changed, and now it would break things to do it 'correctly') or "it was easier" (i.e., at the time the badness wasn't really going to affect anyone, or not measurably, or was very intentional tech debt, and it's only 'now' that anyone is noticing/caring).

In this case, "it was done that way because legacy reasons" is close, but the real answer is "it was done that way because we hadn’t yet invented the parts of compiler theory required to create compilers that enforce this constraint at the type level."
All this compiler sophistication represents a step backwards for binary interfaces. For example, C++ compilers emit such incredible machinery that it's essentially impossible for foreign code to interface with the compiled objects at the binary level. As a result everything eventually gets reduced to the C ABI: simple symbols and calling conventions.
That's... what we're talking about. Simple symbols with calling conventions.

The rules for this proposed ABI are exactly the same as the existing amd64-SystemV C ABI, with one difference: the stack-to-stack copies aren't generated at the call-site; instead, the generated code at the call-site passes the address (in a register, or spilled to stack) for what it would have copied. The compiler generates the stack-to-stack copy in the generated function's prologue, using the address it was passed. Nothing more, nothing less. It's just moving the required location for certain generated code across the linkage, and keeping a temporary alive a little bit longer to make that work. (And in exchange, the temporary that the local stack variable gets put in isn't created at the call-site, so the register-file "pressure" of the change is net neutral.)

This is no more or less complex than the current ABI. It doesn't create more exceptions or edge-cases than the current ABI. It doesn't make the ABI harder to implement. The only thing it does, is choose differently in the matter of a basically-arbitrary choice of where to put some generated glue code (the stack-to-stack copy).

The only practical upshot of this change, is that this enables compilers to sometimes do an optimization that they can't currently do, because doing said optimization would go against the rules of the amd64-SysV ABI (i.e. a caller that pushed a register instead of copying the value wouldn't be an amd64-SysV caller any more, and wouldn't be compatible with precompiled amd64-SysV callees any more; and vice-versa for the callee.)

But if-and-when a compiler does do that optimization, it's internal to the generated function. It doesn't mean that there are two potential callee "signatures" under the proposed ABI. There's only one.

Here's what the proposed ABI would probably say about stack copies:

> "The caller always passes large values by reference; the callee always receives them by reference. If the callee is taking a parameter pass-by-value, then it's up to the compiler of the callee to insert code into the callee's function prologue to turn the passed reference into a stack-local copy of the referenced data."

With that particular legalese, the callee's generated copy is still "required" by the spec, but its effects are now also "hidden" from the caller — i.e. its observable results are no longer leaking across the linkage. Therefore, the compiler is now empowered to optimize out the callee copy, as long as it can ensure the resulting code has observably equivalent results from the caller's perspective.

Note that this isn't anything the person implementing the ABI targeting code in the compiler has to worry about. They just write the code to generate a callee function prologue that does a stack-to-stack copy. It's the person writing the optimization pass that comes after that codegen step, who can now can take that stack-to-stack copy and — static proof of read-only access by the callee in hand — drop it out.

The optimization opportunity being enabled by the change, isn't part of the ABI's spec. The proposed ABI is just about moving the stack-to-stack copy into the callee. What the compiler chooses to do when targeting an ABI where the callee does stack-to-stack copies, is up to the compiler. Presumably, it will do "whatever fiendish things it can" at -O3, and "nothing much different" at -O0. Like usual.

And either way, the linkage itself looks the same. The optimization doesn't change the linkage. Any and all tooling that examines the linkage — debuggers, disassemblers, tracers, etc. — would see the same thing, whether the optimization has occurred or not. Because the optimization isn't part of the linkage; it's int...

I've seen people make bad architectural decisions that now the company is stuck with. And it comes down to just the fact that it was a bad decision, no second guessing needed.

I've also seen "bad" decisions made due to outside constraints. These decisions look like bad decisions, except that if you try to "fix" those decisions, it becomes a lot harder than it looks.

Don't get me wrong, there are plenty of times it was cluelessness. I'm just saying, I find myself going "this is stupid" far more often than it -was- stupid. It might be now, but the reasons for it then sometimes made sense.
Yup, there's also time dependence. Perhaps someone wrote some software in COBOL that is hard to maintain now. But rewritng it may not be worth the opportunity cost now, especially for well-tested systems that have been around for a long time and which have critical failure modes. Sometimes it's better to leave things alone and work around them, even if it results in an uglier design.
> A correctly-specified ABI should pass large structures by immutable reference

is just not possible. CPUs don't know about `const`. So you have to work with the assumption that functions that you call can do anything to their arguments. Thus copies cannot be avoided.

(comment deleted)
The CPU also doesn't know what an ABI is
CPUs actually do know about const; it's called a read-only page.

Besides, that's irrelevant. There's nothing stopping my function from following every pointer on the stack and smashing up its contents; are you going to defend against that, too? If not, how is this any different?

An ABI also has a concept of defined and undefined behaviour. You can design an ABI that is fully protected against abuse but often the performance penalty for that will be huge.

Instead what you'll do is specify the constrained inputs and expected output behaviour. From there you can out anything that violates those constraints as non-conformant. As long as you maintain those constraints between versions, there's no ABI breakage.

Also you can absolutely have constant references in an ABI. There may be ways of ignoring the const depending on how you design the ABI but they will be obvious abuse.

How about those explanations:

It didn't matter before, as compilers were not optimizing as much, code had a much closer 1:1 correspondence to assembly (if you are passing it by pointer and not register, you would want to make that clear in code).

It's much easier to implement in simple compilers. On the side of the callee you don't have to check if you manipulate your arguments, which is generally hard. Being able to manipulate your arguments is another shortcut for keeping the compiler simple. On the side of the caller you don't have to check if you hand out a mutable pointer.

Also finally and most importantly: memory access was much cheaper in terms of cpu cycles. Just look at cdecl: all parameters are passed on the stack instead of registers. Our current calling conventions stem from performance hacks like fastcall that were only optimizing for existing code (you pass big structs by pointer by convention).

Sometimes a mistake is a decision under the assumption that the people intended to use this are smarter / more careful than they are.
> my gut is there is something missing here with respect to non local control flow (like exception handling, setjmp/longjmp, and fibers)

(Post author.)

Mechanically, what happens is essentially the same as what ms/arm/riscv do: the caller creates a reference and passes it to the callee. The only difference is that the callee is more restricted than it would otherwise have been in what it can do with the memory pointed to by that reference. So I don't think that there can possibly be any implications for non-local control flow.

Doesn't the referenced data have to be guaranteed to outlive the callee, which would only be true if the callee is guaranteed to return to the calling scope?

You can get around the immutability of the reference if your compiler implements the ABI with copy on write semantics, which I think is a reasonable compromise. But I'm still not certain how you would handle arbitrary control flow that the compiler may not be able to reason about.

If for example your arguments may be behind const references, how would you implement getcontext/swapcontext for your ABI? If everything is an integral value in registers or on the stack then it's really easy, but i would think it would have to be a compiler intrinsic if it depends on the function signature of the calling context, in order to perform the required copies.

Well for one, the language says a copy is made at the time of the function call, and it's perfectly valid to modify the original before the copy is finished being used. So pretty much any potentially aliasing write or function call in the callee would force a copy, and as he notes C's aliasing rules are lax enough that that's most of them.

Then if you care about the possibility of signal handlers modifying the original... you pretty much have to make a copy every time anyway.

Plus any potential concurrency synchro point existing would force a copy, plus using any unknown function, etc.

Using rust and propagating the single writer xor multiple readers requirement in an ABI, this might be interesting. But with C/C++, I'm afraid copies would be forced "all" the time.

There's still a lot of functions which don't call unknown functions before accessing an argument passed by value, don't take the argument's address, etc. There are many simple functions such as this one:

    void print_foo(FILE *outf, struct foo foo) {
        fprintf(outf, "foo '%s': %i, %i\n", foo.name, foo.x, foo.y);
    }
That one would gain a speed-up and code-bloat reduction from the proposed ABI, and there are many like it.

But even if every single function had to fall back to making a copy, the argument is that there's still a significant code bloat saving by putting the copy in the callee rather than in the caller. After all, the instructions necessary to make a copy takes some space, and with the proposed ABI, those instructions are put in the called function, rather than in every function call. Most functions are called more than once, and all functions are called at least once (hopefully), so anything which can be changed from O(number of function calls) to O(number of functions) is an improvement.

Exactly, see my example elsethread. Also in C and derivatives distinct objects are guaranteed to have distinct addresses. Implicit sharing would break this.
It wouldn't. The compiler would just have to generate the copy when the standard demands it (such as if the function body takes the address of the object).
Yes but then in many cases either (or both!) the caller and the callee might need to make a copy defeating the point of the optimization or even being worse than the original.
In many cases the callee would have to make a copy, yes. However:

1. In many cases, no copy would have to be made. There are lots of small non-complex functions out there where the compiler can prove that it's safe to not make a copy.

2. In many other cases, a copy has to be made. But the copy is made by the callee, not by the caller. That means that all the instructions necessary to copy the argument ends up in the binary once in the callee, rather than once for every function call, leading to less code bloat (which has its own performance advantages).

In fact, a stupid compiler could just always make a copy without analyzing the function body. This would result in a compiler which generates code that's about as fast as it would be with current ABIs, but with a smaller size.

You have to make a copy on the caller or the callee if the address of the object escapes, so you might end up with two extra copies even if nothing in the program mutates the object.
I don't understand how you achieve extra copies? My understanding is that the caller would never make a copy, it would always pass a pointer to large structs. So the absolute worst case, unless I'm missing something, is that we end up with the same number of copies as we do today (i.e one copy per large struct passed as a parameter).
If the address of the object escapes on the caller side then it has to make a copy as the object could be mutated or even just break the distinct address guarantee of the language.
I still don't understand, sorry. If the callee does something which could cause the caller's object to change, such as calling an unknown function or modifying through another pointer which might alias the parameter, the callee would just have to make a copy.

Could you provide an example of a situation where there would be more copies made using the proposed ABI than in traditional ABIs?

Sure, if calling any external function or writing though any pointer would force the callee to copy the object then yes you can have only the callee do the copy, but then it seems that this optimization would apply only to a very small subset of functions.

  struct A { int m; } global = {5};
  int f(struct A a) {
   global.m = 7;
   return a.m;
  }
  
  int main() {
   f(global);
   // need to make a copy of 'global' here
   // otherwise f will return 5 instead of 7
  }
Hey, I've realized that there are two understandings of the proposed ABI: One in which the only promise is that the callee won't modify the object through the pointer, and one in which the callee promises to not modify the object through the pointer and the caller promises that nothing else will modify the object. Maybe you could shed some light on it since you're the author?

In the first version, the worst case situation is that only one copy is made, and it's always made by the caller. However, the caller has to make a copy if the object is referenced after any function is called, because that function might otherwise modify the parameter if a pointer to the caller's version of the object has leaked out somewhere.

In the second version, the worst case situation is that two copies are made where old ABIs would make just one copy (if the caller has to make a copy and the callee has to make a copy). However, the callee would only have to make a copy if it actually does something which might modify the object through the pointer passed as an argument, so the optimization would apply for more functions.

I think it's fairly clear from the article that your intended ABI is the first version, due to the sentence "In the event that a copy is needed, it will happen only once, in the callee, rather than needing to be repeated by every caller" . But in this comment, you're implying that the caller makes a copy if it can't guarantee that nothing else has a pointer to the object?

I should have been clearer; my intention was your second interpretation. The copying happening only once is predicated on the assumption that the struct wasn't aliased; since it's unlikely to be aliased if you're passing it around by value.

Your first interpretation is essentially what the ms/arm/riscv abis do. The reason I don't think that works as well is—

In general, it's rare for functions to mutate their parameters by value. We can effectively treat this as an edge case, and 'compensate' by making copies in the callee when necessary. But, when does the caller need to make a copy?

Version 1: whenever the object is aliased before the call, or read from after it

Version 2: whenever the object is aliased before the call

I think using the same struct multiple times is something that happens relatively frequently, so compared with v1, v2 elides a lot of caller-side copies. In exchange, it adds a relatively small number of callee-side copies. Which, despite the few pathological cases, seems likely to be overwhelmingly worth it most of the time.

Sorry, I messed up. I meant to write that in the first version, the copy is made by the callee. If the copy is made by the callee, then the callee can avoid a copy if it can guarantee that the caller's version of the object isn't changed before the callee uses it, and at most one copy is made.

Anyways, your intention is clear now at least. I'd be a bit worried about an ABI which might produce two copies for one parameter. It would be interesting to analyze a bunch of real-world code and see A) how often would my version create a copy, B) how often does the MS/ARM/RISC-5 version have to make a copy, C) how often would your version make a copy, and D) how often would your version require two copies.

Would also be interesting to see an analysis of code bloat due to copying parameters.

> If the copy is made by the callee, then the callee can avoid a copy if it can guarantee that the caller's version of the object isn't changed before the callee uses it, and at most one copy is made.

So the callee has to know what every caller of it will ever do? That's ... an ABI. The whole point is that functions can exist in a vacuum without knowledge of who they will be called by.

To be clear, I think it would be really cool if compilers could generate ad-hoc calling conventions using lto to optimize spillage, but that's not really useful as an ABI.

> would be interesting to analyze a bunch of real-world code and see A) how often would my version create a copy, B) how often does the MS/ARM/RISC-5 version have to make a copy, C) how often would your version make a copy, and D) how often would your version require two copies.

> Would also be interesting to see an analysis of code bloat due to copying parameters

I agree!

I'm not explaining myself clearly.

The ABI I had in mind was similar to the AArch64 ABI:

>If the argument type is a Composite Type that is larger than 16 bytes, then the argument is copied to memory allocated by the caller and the argument is replaced by a pointer to the copy.

But with a slight modification to put the copy in the callee:

>If the argument type is a Composite Type that is larger than 16 bytes, then argument is replaced by a pointer to the copy. The callee copies the pointed-to object into memory allocated by the callee.

This immediately has the advantage of less binary bloats, because the amount of parameter copying instructions in the binary will become O(number of functions) rather than O(number of function calls). (As an aside: That can probably be a huge advantage for C++ with its large, inlined copy constructors.)

When the copy is made in the callee, we can start identifying cases where a copy isn't necessary, or cases where only certain parts of the struct has to be copied. It would have to be fairly conservative though, since unlike with your ABI, there would be no guarantee made by the caller that there are no other references to the parameter.

I think my version is a clear and obvious improvement over the status quo, with decreased binary sizes and as good or better performance. Your version is more risky where the worst case is two copies per large parameter but, your version will probably achieve zero copies in way more cases than my version. "Low risk / medium reward" versus "medium risk / probably high reward".

---

Anyways, I might end up writing a blog post on this stuff. If I do, it will refer to your blog post. How should I refer to you? Moonchild or elronnd or something else?

One of the things I'm really excited about for Rust is restrict semantics are baked right there into the language with &mut/&. I think there were still a few LLVM bugs to flush out(because very little C/C++ code uses restrict by nature of how it subtly explodes when you get it wrong).

In theory when they sort that out Rust should be able to turn on restrict where it applies globally "for free".

Propagating "noalias" metadata for LLVM has actually finally been enabled again recently in nightly [0]. However it has already caused some regressions so it is not clear whether we may go through another revert/fix in llvm/reenable cycle [1]. This has happened several times already sadly [2] as, exactly as you say, basically nobody else has forged through these paths in LLVM before.

[0]: https://github.com/rust-lang/rust/pull/82834

[1]: https://github.com/rust-lang/rust/issues/84958

[2]: https://stackoverflow.com/a/57259339

Seriously, this has been years now. Is this understandable, or does this tell us something bad about llvm?
Or something bad about noalias?
This is understandable. Rust really uses restrict semantics in anger compared to any other language I know of. Have you seen restrict used in a c codebase? The LLVM support for restrict just doesn't get exercised much outside of Rust.
I use it in c++ on most signal processing stuff. I think eigen will use it if you don't let it use intrinsics for simd. I also use g++ so I wouldn't have encountered it anyway.
Also there are a lot of fortran compilers using llvm now. Fortran has the information for noalias as well.
This is what happens when you're trying to add a hairy feature to a big legacy codebase.
Referring to the LLVM codebase as legacy makes me feel old…
> Is this understandable, or does this tell us something bad about llvm?

LLVM is a large project that's mostly written in pre-modern C++, and "noalias" is a highly non-trivial feature that affects many parts of the compiler in 'cross-cutting' ways. It would be surprising if it did not turn up some initial bugs.

Initial, yes, but this was first uncovered in Oct 2015. That seems like long enough to fix it.
It’s not a single bug, it’s a bunch of different bugs in the interactions between noalias and various analysis and optimisation passes.
Aliasing analysis is a complicated part of the compiler, and it underpins a lot of optimization passes. It’s not an easy thing to bolt on.
TBF the internal API could be designed more pessimistically, as in llvm could drop noalias annotations unless they’re explicitly maintained (/ converted).

This means optimisation phases would need to explicitly opt-in and aliasing optimisations would commonly be missed by default, but it would avoid miscompilations.

Ah damn that's a real shame, something to be said about how rarely restrict is used(I usually only touched it for particle systems or inner loops of components where I knew I was the only iterator).
The other neat thing about Rust is that if you turn on LTO and all of the involved code is Rust, there is no hard ABI. The linker will do all sorts of interesting things with register usage and how to pass data to functions.
Why is that not the case with any other language and LTO?
I think MSVC has some weird things when you call a function and you do this, I don't remember but it had a weird calling convention in the assembly IIRC.
It is. As far as I know, rustc just exposes LLVM’s LTO functionality, without performing any Rust-specific optimizations at link time. So you get the same optimizations as you would with LTO in Clang (C compiler), and indeed you can do cross-language LTO between the two.

Also, even without LTO, LLVM can perform the same ABI optimizations on functions that are local to a single translation unit and not exported to other ones. In Rust that means a function not exported across crate boundaries. In C that means a `static` function.

Besides the sibling comments, this is already highly exploited in JITs for example.

Since only the JIT sees the native code, alongside JIT caches[0] you have basically LTO and PGO data across runs and some call graphs might eventually be collapse into simple basic block, or structs might be reduced to the actual set of fields that are used.

[0] - This includes all modern JVM implementations including the Android cousin, and .NET to certain extent already with usability improvements coming in version 6.

Speaking just on the design of the article, Firefox Reader mode is very useful here
That's funny. I read this in a bright environment with sunglasses, an I had to use Firefox reader mode to even see it.
To each their own. To me it's one of the few articles I didn't have to use the Reader on. It's lightweight, has good contrast and no ads. Very easy to read.
Out of curiosity: wouldn't doing this make semantics between C and C++ a lot nastier since in C++ passing a copy of an object by value implicitly calls the copy constructor from the caller function?

Still, definitely an interesting optimization. I can definitely see a place to use this optimization myself in the near future (custom new ABI) either way.

You can't pass a C++ object with a user-defined copy constructor or a non-trivial default to a C function. If you want to pass an object from C++ to C it has to be POD (plain old data) i.e. effectively a C struct.
Sure, but I was thinking more of how C++ calling conventions typically tend to follow C calling conventions on a given architecture, so AIUI you'd either have to have a differentish calling convention or inefficiently require callees to make copies of already copied objects.

Not that there's anything intrinsically wrong with having a differentish calling convention for C and C++ - it'd just add a smidgen more complexity is all.

That per se wouldn't be a new issue; for example in the Itanium ABI non-trivially copyable objects already use a different calling convention than PODs (specifically they can't be passed via registers and need need to be passed via hidden pointer).
Hmm, I don’t agree. If you pass a structure by value, that is supposed to make a copy. The callee is free to modify the copy any way it likes (unless you use const, and that’s not much protection in C). Doing as the author suggests, and passing in an immutable parameter, would introduce copy-on-write semantics.

If the concern is the overhead of copying large structures around, the obvious solution is to not do that and simply pass structure pointers (like most APIs do). This also gives the API implementer (i.e. callee) full control over whether copies get made or not.

In fact, I think I’d argue that if you’re passing large structures by value, Your API Is Probably Wrong and this is absolutely not the ABI’s problem in the first place.

> Doing as the author suggests, and passing in an immutable parameter, would introduce copy-on-write semantics.

Yes, and what's wrong with that, if it's transparently done by the compiler?

In this scheme, the caller would pass non-register-sized data "by value" by actually passing a pointer to it. (Importantly, this pointer is register-sized, and so wouldn't necessarily need to be spilled to the stack—unlike caller memory copy, which is always to the stack.)

A callee that can be statically determined to never modify the value, would, under this calling convention, be compiled to code that simply works with the data "through" the pointer that's been placed in its register / on the stack.

A callee that can't be statically determined to never modify the value, would, under this calling convention, generate a memcpy from the pointer onto the stack; where the pointer then goes dead at that point (and so, if the pointer was spilled to the stack by the caller, then the memcpy could be targeted so as to overwrite the pointer on the stack.)

This would be much more efficient under most conditions. Its only inefficiency would come from the situation where the pointer being passed would necessarily be spilled to the stack; and then the callee would be statically guaranteed to make a copy. In that case, you're doing an extra push (for the pointer) that current ABIs avoid by just having the caller do an eager copy.

But this would be quite rare in practice, since this ABI (and the ABIs it replaces) are only for external symbols — the kind whose linkage is fundamentally dynamic (i.e. where the linker-loader could in theory substitute anything it likes for the external symbol with an LD_PRELOAD shim.)

Internal symbols — private static functions — get bespoke compiler-specific ABIs that skip all this enforced caller/callee predetermined role business and just codegen whatever's most efficient on a case-by-case bases, with different callsites getting different monomorphizations or partial inlinings of the callee that distribute the responsibilities differently.

And since these ABIs only matter for external linkage, you have to remember that symbols with external linkage get no "type attribute enforcement" at link-load time, and so your symbol for a function that was originally guaranteed to be a callee-that-always-writes, might be substituted at link-load time for a callee-that-never-writes. In which case, having the pointer on the callee's stack once again becomes handy, rather than useless.

> In fact, I think I’d argue that if you’re passing large structures by value

Not necessarily large structures. More often things like UUIDs—values just large-enough to not fit into a (64-bit) machine register. It's these small memory spills, done in hot loops, that add up. There are huge wins for the cleanliness of e.g. RDBMS engine code, if their various 128-bit to 512-bit column types can be passed by value without necessitating an eager copy.

A problem would be that performance becomes brittle. So you change the strict in your function and suddenly the program is slower.
Other parts of the program can modify the object after the callee has started, if the callee contains some synchro there would be no race. At this point maybe you can think of additional tricks like CO"W" before any potential synchro point if some exist, but I'm not sure big advantages would remain except if you very carefully craft your program knowing all of that for the opti to happen, and it seems a lot of complexity in the compiler for limited gains, especially since if you have to massage the code you could as well transform to a reference yourself at source level (and that would better resist to e.g. calling unknown functions from the callee)
In order for another part of the program to do that, you would already have had to generate a mutable pointer to the object somewhere. As the article actually already goes into, this is something the compiler already tracks in order to know if a value needs to be loaded from memory when using it again.

If the ABI specified that objects passed this way were passed by immutable pointer, then just passing the object using this new method doesn't generate a mutable pointer, so the compiler is free to assume nobody else can modify it.

But that means the caller also has some proofs to do, if a counter example is found or nothing can be proven the caller also has to do a copy, and that's with no guarantee the callee won't do another one...

So not really an optimization anymore.

You say "proofs" but, as I said, this is already something the compiler keeps track of.

The caller is required to do a copy as the ABI is right now so if you can't do this optimisation it's only as bad as it was before.

Yes, there is a case where this creates one extra copy. The case where you a) give away a mutable pointer to a variable before calling another function, and b) the called function modifies it's own argument and then c) that function then doesn't use that object as the argument for another function it calls.

That's the only case where it generates an extra copy, and I'm going to go out on a limb and say that I'm almost cirtain that this will reduce sigificantly more copies that it creates.

Even if the called function (call it B) does use the object as an argument for another function it calls (call it C), B could end up having to perform another copy for that call, if B also takes a pointer to the variable at some point and lets it escape. Or a pointer to any part of the variable.

And it’s not limited to mutable pointers. Sure, if the variable itself is marked const, then it’s UB to modify it. But const local variables are rare in most codebases, in my experience. If on the other hand you pass the address of a non-const variable as a function argument, even if the type of the argument is a const pointer, the function can legally cast away the const and write to the pointer.

Still, I agree that this would probably eliminate more copies than it creates. But I don't think it would eliminate or create very many copies. In my experience, approximately nobody writes code that passes structs by value in the first place.

You know what this would mostly affect? Code that passes longs or doubles by value, when compiled for 32-bit architectures (like many modern ARM microcontrollers still are.) Something doesn’t have to be a struct to be larger than a machine register.
Also, FYI (sibling post because my first one is already locked): this isn't about what the programmer tells the compiler. It's about what the compiler can infer on its own, through dataflow analysis.

"const", as a qualifier, isn't novel information about a variable for the compiler — compilers already know whether a variable is used mutably or not, just like they know when and where a variable has its last access and goes dead.

"const" is an instruction to the compiler that if it finds, by its own independent analysis, that the variable cannot be const in that context, then the program is invalid and compilation should error out.

As such, "const" only even has a meaning, if the compiler already independently knows which declared variables are or are not const "in practice."

That information is what would be used to decide whether to do the copy.

> B could end up having to perform another copy for that call

At worst, that's no worse than what happens under the current ABI, where A passes data by value to B, which passes it by value to C.

(Do also remember, as I said in another comment, that ABIs/calling conventions are only "about" functions with external linkage. Functions with "static" linkage do whatever the compiler likes; as do their callers.)

I think, in the specific situation you're describing, there's also prelink-time cross-unit optimizations that can be done here to eliminate redundant copies even further — but that's getting close to just expecting C to do Rust-like ownership analysis, so I wouldn't claim that to be practical in any production compiler in the near future.

(comment deleted)
That is a nightmare. You are telling me that the size of the stack of a function now depends on the fact that I modify an argument?

Also, you are masquerading an access trough a pointer as a direct access to some data. An access trough a pointer is less efficient than a direct access, and thus one could assume that is doing something efficient while in fact it isn't.

Finally, who guarantees that the programmer doesn't abuse this thing and starts to modify the data without doing the copy on write? On purpose or by mistake, the effect is that passing by value something will result in that something being mutated.

In the end, what is the problem of passing a pointer? It's more explicit and you know what is going on.

> There are huge wins for the cleanliness

What cleanliness? Because you think that

    void f(T *x);
    f(&x); 
is not clean? Well, then use C++:

    void f(const T &x);
    f(x);
You cannot design an ABI that is supposed to be used by all languages on the fact that doing something in a specific language is more ugly than doing some other thing. If it's a problem of C, we should fix C, not change the ABI to change the semantic of C.
> An access trough a pointer is less efficient than a direct access

But the alternative is not "a direct access." It's a copy and then direct access. A copy, mind you, that occurs through a pointer to grab the original data from the stack position it was sitting in. That's the very same indirect read that the callee does in the proposed ABI!

Thus, the indirect memory read will happen either way; either at caller copy time (existing ABIs), or at read time (proposed ABI, copy optimized away), or at callee copy time (proposed ABI, copy not optimized away.)

The only difference in the number of indirect memory accesses, with the proposed ABI, is that you might be able to avoid an indirect memory write (i.e. of the memory the caller is writing to by doing the copy.)

> Finally, who guarantees that the programmer doesn't abuse this thing and starts to modify the data without doing the copy on write?

Uhh... we're not talking about features exposed to the developer. We're talking about calling conventions—instructions to the compiler on how to distribute the glue code it generates between the call-sites it builds ("caller-[verb]ed" things) and function prologues/epilogues it builds ("callee-[verb]ed" things.)

The "copy on write" part here [if you want to call it that—it's not "on" write] would be part of such a generated function prologue ("callee-copied")—the same code that gets generated at the call-site in existing calling conventions ("caller-copied"), just moved to the other side of the linkage. With the important difference that, if the compiler could prove that the callee (or anything it calls, passing the variable, transitively) wasn't going to do anything write-like with the data behind the pointer, then the copy in the function prologue could be optimized out.

This compiler optimization can only occur if the copy is the callee's responsibility. Given external linkage (think: separate compilation units, e.g. calling a function defined in a static library that's already compiled and only available in binary form), the caller can't guarantee the properties of the callee. But a callee can always guarantee its own properties during function-prologue/epilogue generation—since it's generating the function prologue/epilogue in the very same compiler session that has access to the AST that describes the function's static-analysis-proven properties in full.

(Yes, this means that a compiler could be mistaken about whether a callee writes through the implicit pointer underneath the passed value, and therefore generate the optimized function prologue when it shouldn't. This is known as a compiler bug, and wouldn't be something J Random Hacker would ever run into if using a stable, non-alpha release of their compiler, with a stable, non-alpha release of support for this calling convention.)

> What cleanliness?

The functionality being proposed is that you just pass-by-value (which makes obvious that the semantics your code is enacting is "the function can do whatever it likes with the value, and it won't affect the caller, because if it writes, it's writing to a copy of the value"); but sometimes it'll be automagically faster than it could have otherwise been, because sometimes, when the compiler can prove certain properties, no copy — only an indirect memory read — would be occurring.

This allows you to always use pass-by-value when you want those semantics, and to get code that's "as optimal as it can be"; rather than having to pass pointers around and explicitly copy-on-write and all that mess in order to get that same level of optimization.

(And yes, that's the alternative in heavily performance-oriented code like a database's tuple materialization logic. You don't get to write clea...

> But the alternative is not "a direct access." It's a copy and then direct access. A copy, mind you, that occurs through a pointer to grab the original data from the stack position it was sitting in. That's the very same indirect read that the callee does in the proposed ABI!

Depends. If the structure was constructed in the caller and not needed afterwards the compiler could choose to directly construct it in the stack space used for the function call.

> What cleanliness? Because you think that > > void f(T *x); > f(&x); > > is not clean? Well, then use C++: > > void f(const T &x); > f(x);

To be honest, the first option is much better for readability. Visual inspection of the caller lets the reader know whether or not f can modify x.

That second option is horrible for the maintenance programmer who is reading the caller; whether or not you are interested in what the function f() does, you need to actually look it up to see if it is allowed to modify x.

Copy-on-write is exactly what G++ and libstdc++ did with std::string before C++11 changed the spec. And it was terrible and full of bugs.

I remember how I had to solve a pile of thread bugs in a C++ project by changing string assignments into iterator constructors which would bypass the GNU CoW reference counter.

They tried, and tried, but never quite succeeded in fixing every single possible race condition with the reference count CoW implementation.

Copy-on-write is not fundamentally what we're talking about.

Copy-on-write is "the called function receives the original, but if it needs to write, then there's explicit code that makes a copy and replaces the reference to the original with a reference to the copy."

What we're talking about here is:

• A function is being compiled. It receives one of its formal parameters "by value." That parameter's data is larger than a machine-register in size.

• The compiler uniformly generates the call-sites of this function to provide the address of the original data, passing it in a register. The call-sites of the function look the same whether or not the function does the copy.

• The compiler, by static analysis of the function, determines whether the function really needs a copy—i.e. whether it would make a difference to the code whether the copying ever happens.

• If it needs a copy, then the compiler generates the function to have a prologue that does a copy from the address in the caller-passed register to the address in the stack pointer; and then the compiler generates code that interacts with the copy as a local stack variable. (Because, after the copy, it is!)

• But if the function doesn't actually "need" a copy, then the compiler generates no copy in the function prologue, and generates code for the function body that interacts with the original indirectly through the passed address in the register."

This has nothing-at-all to do with copy "on" write. This is "copy now, if we can't guarantee there won't be a write later."

Which is already what compilers do. That's what "pass by value" means. It's just that current calling conventions restrict compilers from being able to ever make that guarantee; whereas under the proposed calling convention, they would sometimes be able to make that guarantee.

(comment deleted)
> In this scheme, the caller would pass non-register-sized data "by value" by actually passing a pointer to it. (Importantly, this pointer is register-sized, and so wouldn't necessarily need to be spilled to the stack—unlike caller memory copy, which is always to the stack.)

Sane ABIs already pass by-value parameters via up to two registers (or via vector registers). One register is just to small for many common things like 3D/4D vectors or start+end pointer pairs.

> Internal symbols — private static functions — get bespoke compiler-specific ABIs that skip all this enforced caller/callee predetermined role business and just codegen whatever's most efficient on a case-by-case bases, with different callsites getting different monomorphizations or partial inlinings of the callee that distribute the responsibilities differently.

Unfortunately this is still an area where compilers could do a lot more.

> and passing in an immutable parameter, would introduce copy-on-write semantics.

Yes that’s the point the author is making. His scheme incurs strictly less overhead than the scheme currently used.

Some people may want to program in a “value oriented” way without pointers at the C level for various reasons but for efficiency reasons they cannot.

The explaination by derefr is correct.

But there is a larger assumption that underlies your post that I think is important to correct.

You seem to be caught up in the idea that the code you write, and the code the compiler generates must have some kind of one-to-one corrospondance, but this isn't the case at all.

The code the compiler generates must have the same final result, but it doesn't have to actually work the same way.

So if you pass a structure by value, and the compiler can avoid a copy somehow because it's more efficient, then of course it should do so.

Making an ABI that is ammenible to allowing the compiler to make better optimisations is of course also a good idea. And the proposed ABI here achieves that.

> The code the compiler generates must have the same final result, but it doesn't have to actually work the same way.

You're obviously correct, but I think this ignores downstream costs of sufficiently magical compilers. If my compiler tunnels into a parallel universe, executes the code there, and returns the result, that's fine, but it's going to be enormously painful to debug.

Obviously very few developers (including myself) have an appropriate mental model of highly complex, speculative processor architectures that end up executing their code, but I do think there is at least some benefit to having a correspondence between the code you write and the code the compiler generates.

Maybe it's not a problem with the compiler, but a problem with languages. Maybe we just need better languages?

Doesn't this hurt forward compatibility to new architectures where there are more registers and it would be more efficient doing it through registers? Seems like automatic choice by the compiler should be default and if explicit control is needed some kind of attribute or something could be used to override the compiler's choice.
Not really because ABI's are already architecture specific so they already do stuff different for other CPUs. Similarly, their are already a variety of attributes that can be used to explicitly control the ABI of the top of my head things like packed structs and function calling convention are common and in several languages (C, C++, rust and zig).
> You're obviously correct, but I think this ignores downstream costs of sufficiently magical compilers. If my compiler tunnels into a parallel universe, executes the code there, and returns the result, that's fine, but it's going to be enormously painful to debug.

> Obviously very few developers (including myself) have an appropriate mental model of highly complex, speculative processor architectures that end up executing their code, but I do think there is at least some benefit to having a correspondence between the code you write and the code the compiler generates.

It’s not really clear to me why you think this? Or at least, it’s not clear to me how you can write this without levelling the same complaint against essentially any C compiler written after the mid 1980s?

Because for all the hand—wringing in the comments here about compilers generating code that doesn’t do what your C said it would do in the way you wrote it, that ship sailed decades ago.

That quaint little integer addition loop you write in C will, as like as not, be exploded by the compiler into an unrolled stream of SIMD instructions and packed registers that doesn’t resemble what you wrote in the least, which the CPU may then further scramble and interleave, resulting in nothing remotely like what a naive person might imagine is a reasonable translation of what you wrote – and yet, debuggers remain broadly useful tools.

And people have for decades now shown a strong desire to enthusiastically and with great speed drop compilers in favour of newer ones that performed better optimizations, which is to say, people have shown a strong preference for compilers that are good at generating code that looks nothing like what you wrote, provided the results are the same and the performance is as fast as possible.

Turning a copy into passing a pointer that may later copy if and only if the compiler sees the possibility of a write coming is, in terms of modern optimizations, an absolutely conservative optimization that doesn’t routinely happen only because platform ABIs unnecessarily rule it out. Far from being a bridge too far, in terms of modern compiler behaviour it’s simple and low-hanging fruit which isn’t exploited even though far more exotic tricks are already the rule of the day.

GCC and LLVM already do this, the idea of what you wrote is still there, if you want to debug add debug info, DWARF exists for a reason.
Dropping one-to-one correspondence is fine for most code, but there are certain places where it is unacceptable. An ABI is one of these places, because the interface is designed to be exposed in a way that is observable.
> The code the compiler generates must have the same final result, but it doesn't have to actually work the same way.

It doesn't, but when it comes to debugging, it would help to have them similar.

My reading of the C++ standard is that this behavior is effectively mandated and that one can write a program which can tell if an ABI observed the proposed optimization.

[expr.call]: "The lvalue-to-rvalue, array-to-pointer, and function-to-pointer standard conversions are performed on the argument expression."

[conv.lval]: "... if T has a class type, the conversion copy-initializes the result object from the glvalue."

The way a program can tell if a compiler is compliant to the standard is like so:

  struct S { int large[100]; };

  int compliant(struct S a, const struct S *b);

  int escape(const void *x);

  int bad() {
    struct S s;
    escape(&s);
    return compliant(s, &s);
  }

  int compliant(struct S a, const struct S *b) {
    int r = &a != b;
    escape(&a);
    escape(b);
    return r;
  }
There are three calls to 'escape'. A programmer may assume that the first and third call to escape observes a different object than the second call to escape and they may assume that 'compliant' returns '1'.
The compiler would be forced to create copies in that case. In general (using my proposed ABI), taking the address of an object will cause this, because it is possible to mutate an object through its address.

It's still a win because 1) you can avoid making copies in many places, and 2) code size decreases because the copy happens one time in the callee rather than many times for every caller.

So the caller might have to copy if the the pointer escapes and the callee might have to copy if it needs to mutate the value. In practice in many cases you might end up with more copies than the "bad" ABI.
But similar things can already happen where copy elision is optional. It's one the niches the C++ standard carves out regarding the as-if rule, and adding one for this new purpose is conceivable.
Copy elision is very different as it to only ever avoid copying objects that are just about to be destroyed.
"A correctly-specified ABI should pass large structures by immutable reference"

There is no such thing at the level that the ABI works, especially at a kernel-userland boundary (where the choice is to fully marshal the arguments or accept that you have a TOCTOU issue).

Callee saved registers kind of work like that. You either don't touch those registers, or you restore them before returning.
An ABI is a contract. If it wants to say that a pointer is an immutable reference, all it needs to do is to say "this pointer is an immutable reference", and specify what that means. It is up to those who follow that contract to follow those rules.

You ABI could say a pointer may only be accessed on Wednesdays if it really wanted to.

Wednesdays in what timezone and by what clock?
Even if we assume the computer's own internal clock, we would have a race condition if the computer goes to sleep on Wednesday afternoon just after a call to that function is done, and woken up in the next day. Sounds like a really impractical ABI.
That's a shame, because imagine how useful it would be to have pointers you can only access on Wednesdays.
Why would that optimization not be possible with a simple const *?

The problem I see with that proposal is that it introduces a new type of pointer, the immutable pointer. That seems like equal to a const pointer, but it's not. With a const pointer the callee can not mutate the pointee. But it can still be mutated from the outside. That means that any time you handed out a mutable pointer to something you have to make a copy for handing out an immutable pointer to it. An ABI like this would probably be much more complex to implement for a small gain.

You would end up with hardly predictable behaviour wether the struct gets copied or not. C# structs suffer a lot from this, because methods are mutating by default (https://codeblog.jonskeet.uk/2014/07/16/micro-optimization-t...). The biggest problem is simply that this is not explicit.

Also there is a case where a calling convention like this can make things worse, as you will have to make two copies:

    void fn1(A*);
    void fn2(A a) {
      // Have to make copy here because mutating
      fn1(&a);
    }
    void fn3()
    {
      A a;
      fn1(&a);
      // Have to make copy here because reference to a might have escaped.
      fn2(a);
    }
> With a const pointer the callee can not mutate the pointee. But it can still be mutated from the outside

That's incorrect. In c semantics, at least, it is legal to take a pointer to non-const, cast it to a pointer to const, pass it back, and mutate the object pointed to. It is only illegal to mutate if the pointer actually points to const memory.

Which means that, in a general sense, if you're given a const pointer, since you can't know if it actually points at const memory, you shouldn't mutate through it. But if you're handing out const pointers to non-const memory, you shouldn't count on the memory not being changed through those pointers.

IOW const is all but useless in c (except as a declaration of intent).

> An ABI like this would probably be much more complex to implement for a small gain

Why? Mechanically, it's almost the same as the ms/arm/riscv abi, except that a copy is made by the callee rather than the caller.

Const matters to the caller, if it calls a function that takes a pointer to const it can generally reasonably expect that the object wont be changed.

It is not about enabling compiler optimisations but about preserving invariants.

Are you saying that in C a callee can cast a const pointer to non-const and write to it and that's sanctioned by the Standard?
Yes, as long as the original pointer was to a non-const location. That is:

  void mutate(const int *x) {
   *(int*)x = 5;
  }
  
  int main() {
   int a;
   mutate(&a); // ok
   const int b;
   mutate(&b); // undefined behaviour
  }
> Yes, as long as the original pointer was to a non-const location.

So then actually no - it's not guaranteed to be portable. That non-const location qualification of course means portable code cannot just cast const pointers to non-const.

> Unfortunately, that doesn’t work anymore; compilers are smart now, and they don’t like it when objects alias.

Let's be specific: compilers for languages that support aliasing. For example, FORTRAN does not permit aliasing and therefore has all sorts of optimizations that languages that do permit aliasing cannot have.

It's a tradeoff like any other, and isn't specific to a compiler beyond the fact that a given compiler X can compile language Y.

But Fortran has its own ABI, while a believe this article is only about C ABI.
There is no such thing as C ABI, rather the OS ABI, which on OSes written in C happens to be the C ABI.

This is specially noticeable in OSes outside the UNIX culture, where two C compilers might not share the same ABI for all its features, while having the same ABI subset for OS libraries and syscalls.

Am I the only one reading this article as disguised PR for a new trendy language?
Yes. It's an article talking about a better calling convention for C, the opposite of a new trendy language.
Interesting observation. I looked into it a bit.

Could theoretically be for Rust, but it sounds like Rust doesn't have the aliasing optimizations working yet. Based off [1], it looks like Swift's ABI might be able to take advantage of these things. (I don't know much about Swift) Based off [2], it looks like Zig definitely can do these sorts of optimizations. Ada also has aliasing constraints, (you need to specify to the compiler that you want a thing to be aliasable) but I don't know if any compilers use them for optimizations. I know that Fortran can do these sorts of optimizations. Both Ada nor Fortran seem to be undergoing a PR renaissance, so they might be "trendy new languages" for that purpose.

However, given that the other two articles on the author's website [3] are about Delphi and complaining about AT&T assembly syntax, I'd suspect that the author is just a system's programmer annoyed about ABIs.

[1] https://gankra.github.io/blah/swift-abi/#ownership [2] https://ziglang.org/documentation/0.2.0/#Type-Based-Alias-An... [3] https://elronnd.net/writ/

For a copy on write const ref ABI trick to work, the referenced area has to be stable while the callee is running. Good luck with guarantying that in C or C++ (and no, it would not necessarily be a race for the refed object to be concurrently unstable, the called function could have internal synchro...)

So I think this idea is virtually impossible. Your ABI is probably not wrong, but your blog post probably is :P

Well, no. The callee would just have to make a copy in cases where the memory might change.

The compiler would have to produce a copy in the case of this function:

    void foo1(struct large l) {
        bar();
        baz(l.x + l.y);
    }
Because the call to `bar()` might change the caller's copy, so the callee `foo1` has to make a local copy before calling `bar()`.

But it would not have to produce a copy in this example:

    void foo2(struct large l) {
        baz(l.x + l.y);
    }
Because nothing can change the caller's copy before the callee `foo2` is done using it.
This isn't correct.

The compiler would not have to produce a copy in either case.

The call to bar() can not change the value of l because the person who called foo1 gave you an immutable pointer to it.

How did foo1's caller know it was immutable?

If the value passed to foo1 was a local variable, or one of it's arguments passed to it by immutable pointer, then all the compiler has to do is check if, in that function, it created a mutable pointer to the value and put it somewhere accessable to someone else. If it did not, then it can just pass the pointer right through.

If it did create a mutable pointer, or if the value came from some other place the compiler has no knowledge about, then it would make a copy of the value before passing a pointer to foo1, just like it would do for the old ABI.

I don't think you're correct here, but it's totally possible that I'm misunderstanding something.

When generating code for `foo1`, the compiler has to know how the parameters are laid out. The code generated for `foo1` can't depend on the code which calls `foo1`. It is my understanding that the proposed ABI says that `foo1` will get `l` passed with a pointer, and that the term "immutable pointer" just means that `foo1` isn't allowed to modify the object through the pointer.

The context (invisible to foo1) could look like this:

    struct large *lptr;

    void bar() {
        lptr->x += 10;
    }

    void do_thing() {
        struct large l = { ... };
        lptr = &l;
        foo1(l);
    }
So the compiler must assume that the call to `bar()` can modify the object pointed to by the "immutable pointer".

---

Or are you suggesting that the callee's decision on whether to make a copy or not should be a runtime decision, and that the caller passes in information about whether the callee has to copy or not?

As you know, in do_thing you have taken a mutable pointer to l and placed it in a location where someone else could access it.

Thefore, before the call to foo1, the compiler simply creates a copy of l, and then passes an immutable pointer to that to foo1. Effectively falling back to how the old ABI worked.

The code generated for foo1 does depend on the code that calls foo1. That is what an ABI is. An ABI is a specification of the contract between the caller and the callee.

The proposed new ABI says "If you are passing a struct by value, then you, the caller, will give me a pointer that you guarantee can't be modified until I return. In turn, I guarantee that I will not modify the pointer you give me."

I think I understand what you're saying now, and where the confusion is.

My understanding of the ABI was: "The callee will always receive a pointer to the caller's copy. If the callee can't guarantee that nothing modifies the caller's copy, the callee makes a copy."

Your understanding is subtly different, because the caller guarantees that nothing else has a pointer to the parameter (and the caller guarantees that by making a copy when that's the only option).

To be honest, I think the ABI according to my understanding is better. It does reduce the amount of cases a copy can be avoided, but it means that the worst-case is that one copy is made (by the callee). With your version, you would have a bunch of situations where the caller has to make a copy (because a pointer has escaped) and the callee has to make a copy (because it modifies the object). With my version, the worst-case is that a copy is made once (by the callee), but the worst-case would be hit more often.

I think my understanding is closer to the intent of the article, due to this sentence: "In the event that a copy is needed, it will happen only once, in the callee, rather than needing to be repeated by every caller." In your version, the copy is sometimes made in the caller and sometimes in the callee.

Look at the very next sentence.

> The callee also has more flexibility, and can copy only those portions of the structure that are actually modified.

It's clear that the author understands that the callee needs to copy any value it wants to modify.

But also, your understanding doesn't actually work, due to the issue you mentioned. So I think it's pretty unreasonable to assume that is what the author meant.

Wait what issue are you referring to? My version absolutely works, it just falls back to making one copy a lot.

In my version the callee can absolutely make a copy of only the modified fields if the function A) modifies some fields but not others and B) doesn't do anything which demands a full copy.

Anyways, discussion on the intent of the article isn't super productive, I wrote a reply to https://news.ycombinator.com/item?id=27091726 which asks the author what their intention was.

Sorry, you are correct. I was thinking about the post at the top of this chain that wasn't actually made by you.
> My version absolutely works,

Consider:

  void other_thread(struct large* l) {
    struct large new_l = { ... };
    *l = new_l;
    }
  void do_other_thing() {
    struct large l = { ... };
    thread_t t = start_thread(other_thread,&l);
    foo1(l);
    wait_for_thread(t);
    }
Not only can `foo1` never avoid making a copy (because it can't assume it's unknown caller is not this perverse), it can't even make a copy safely unless its caller allocates a new `struct large` for it to copy from. (And the caller does not, in general, know that `start_thread` (or `process_thingy_maybe_concurrently^W^W`) starts a thread with the aliased `&l`, so it would have to make such copies on any substantially escaping alias just to be sure, at which point you're back to the original proposal.)

The pass-by-non-mutatably-aliased-reference ABI seems terrible IMO, but it at least allows the callee to decide whether to copy using only information that's actually present in the callee.

"Well, what’s wrong with that? Surely we can just do what we did in the days of dumb compilers and pass structures by pointer. Unfortunately, that doesn’t work anymore; compilers are smart now, and they don’t like it when objects alias. " What does that even mean? Does gcc cry after I make it translate code with pointers, I simply don't get it.

  struct X { int i; };

  X* p;
  void bar(X x){
    P->i=1;
    assert(x.i==0);// fails   with transparent pass by ref
  }
  void foo() {
    X x{0};
    P=&x;
    bar(x);
  }
The compiler knows that there is a mutable pointer to x floating around as soon as you did `P=&x`. Therefore it knows it needs to make a new copy of x and then pass an immutable pointer to that value when calling `bar`.

This is something the compiler already keeps track of anyway in order to be able to know if it needs to re-load variables from memory when they are used again after a function call.

Basically the optimsiation degrades to exactly what happened before in the case where you do this.

In many cases you might need to make two copies. Also remember, the author proposes to change the abi to encourage pass by value instead of of pass by restricted reference, so in the original code there might be zero copies.

In practice this optimization would be very fragile (like most optimizations relying on escape analysis) and people would keep passing by reference in fear that a small change like taking the address of an object might force a copy of a large struct.

Well, no, that's just one of the situations where the compiler would have to make a copy in the callee; a write to a pointer which might alias the argument.
I think clang's noescape attribute for pointer function parameters are somewhat related. There are several compiler extensions that allow refining calling conventions, so clearly even compiler vendors think that there are things to explore here.

Having said that I think the suggestion would be observably break both the current C and C++ standards, so something like this shouldn't be done without an explicit attribute.

So, consider passing a struct A stored in the global variable “foo” to the function “bar” in the parameter “baz”. This function makes a modification to the global “foo”. The compiler cannot know this happens and so the value of “baz” is now affected by this modification even though it should have been a copy. Zig experimented with just this and got problems. So as a solution it seems broken.
the abi is very constraining.

a much better solution of course is to only apply it when strictly necessary, which seems not to be the case de facto in the wild

this is one very easy way to be 'faster than c' in practise

Seems like this whole idea would be easy enough to test. Just compile something like the Linux kernel this way and see if it's actually better.