13 ms·
ABI Mistakes
- hctaw 5y agoI'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).
- lostcolony 5y agoI 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).
- derefr 5y agoIn 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."
- matheusmoreira 5y agoAll 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.
- derefr 5y agoThat'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 internal to the codegen of the callee, enabled by the (uniformly!) modified structure of the linkage.
- david422 5y agoI'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.
- lostcolony 5y agoDon'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.
- rsj_hn 5y agoYup, 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.
- jcelerier 5y ago> 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.
- deleted 5y ago[deleted]
- mhh__ 5y agoThe CPU also doesn't know what an ABI is
- wizzwizz4 5y agoCPUs 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?
- jacoblambda 5y agoAn 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.
- legulere 5y agoHow 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).
- jbverschoor 5y agoSometimes a mistake is a decision under the assumption that the people intended to use this are smarter / more careful than they are.
- moonchild 5y ago> 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.
- hctaw 5y agoDoesn'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.
- brigade 5y agoWell 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.
- temac 5y agoPlus 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.
- mort96 5y agoThere'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.
- gpderetta 5y agoExactly, see my example elsethread. Also in C and derivatives distinct objects are guaranteed to have distinct addresses. Implicit sharing would break this.
- mort96 5y agoIt 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).
- gpderetta 5y agoYes 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.
- mort96 5y agoIn 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.
- gpderetta 5y agoYou 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.
- vvanders 5y agoOne 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".
- nathankleyn 5y agoPropagating "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 https://github.com/rust-lang/rust/pull/82834 [1]: https://github.com/rust-lang/rust/issues/84958 https://github.com/rust-lang/rust/issues/84958 [2]: https://stackoverflow.com/a/57259339 https://stackoverflow.com/a/57259339
- galangalalgol 5y agoSeriously, this has been years now. Is this understandable, or does this tell us something bad about llvm?
- wnoise 5y agoOr something bad about noalias?
- swsieber 5y agoThis 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.
- galangalalgol 5y ago
- mssundaram 5y agoSpeaking just on the design of the article, Firefox Reader mode is very useful here
- SixDouble5321 5y agoThat's funny. I read this in a bright environment with sunglasses, an I had to use Firefox reader mode to even see it.
- Narishma 5y agoTo 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.
- haneefmubarak 5y agoOut 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.
- zabzonk 5y agoYou 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.
- haneefmubarak 5y agoSure, 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.
- gpderetta 5y agoThat 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).
- nneonneo 5y agoHmm, 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.
- derefr 5y ago> 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.
- chjj 5y agoDMR on `restrict` (originally proposed for C89 as `noalias`): http://port70.net/~nsz/c/c89/dmr-on-noalias.html http://port70.net/~nsz/c/c89/dmr-on-noalias.html
- cokernel_hacker 5y agoMy 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'.
- moonchild 5y agoThe 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.
- gpderetta 5y agoSo 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.
- MauranKilom 5y agoBut 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.
- 5y ago
- foobiekr 5y ago"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).
- legulere 5y agoCallee saved registers kind of work like that. You either don't touch those registers, or you restore them before returning.
- user-the-name 5y agoAn 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.
- omnicognate 5y agoWednesdays in what timezone and by what clock?
- varajelle 5y agoEven 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.
- user-the-name 5y agoThat's a shame, because imagine how useful it would be to have pointers you can only access on Wednesdays.
- legulere 5y agoWhy 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-the-surprising-inefficiency-of-readonly-fields/ 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); }
- moonchild 5y ago> 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.
- gpderetta 5y agoConst 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.
- gumby 5y ago> 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.
- varajelle 5y agoBut Fortran has its own ABI, while a believe this article is only about C ABI.
- pjmlp 5y agoThere 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.
- stephc_int13 5y agoAm I the only one reading this article as disguised PR for a new trendy language?
- mort96 5y agoYes. It's an article talking about a better calling convention for C, the opposite of a new trendy language.
- genrez 5y agoInteresting 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 https://gankra.github.io/blah/swift-abi/#ownership [2] https://ziglang.org/documentation/0.2.0/#Type-Based-Alias-Analysis https://ziglang.org/documentation/0.2.0/#Type-Based-Alias-An... [3] https://elronnd.net/writ/ https://elronnd.net/writ/
- AndyKelley 5y agocorrection: https://ziglang.org/download/0.4.0/release-notes.html#Type-Based-Alias-Analysis-Status https://ziglang.org/download/0.4.0/release-notes.html#Type-B...
- temac 5y agoFor 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
- mort96 5y agoWell, 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.
- Negitivefrags 5y agoThis 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.
- mort96 5y agoI 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?
- colour 5y ago"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.
- re 5y ago"Doesn't work anymore" is a hyperbole. Aliasing can prevent optimizations, like in the code example immediately after that paragraph where the value of `x` needs to be loaded from memory to be returned. See also https://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization https://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts...
- gpderetta 5y agostruct 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); }
- Negitivefrags 5y agoThe 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.
- gpderetta 5y agoIn 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.
- mort96 5y agoWell, 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.
- leni536 5y agoI 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.
- lerno 5y agoSo, 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.
- jheriko 5y agothe 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
- neerajsi 5y agoSeems 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.