6 ms·
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 co
by nneonneo 5y ago
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.
- 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.
- andi999 5y agoA problem would be that performance becomes brittle. So you change the strict in your function and suddenly the program is slower.
- Negitivefrags 5y agoThat war was lost decades ago.
- temac 5y agoOther 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)
- Negitivefrags 5y agoIn 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.
- temac 5y agoBut 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.
- CyberRabbi 5y ago> 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.
- Negitivefrags 5y agoThe 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.
- __jem 5y ago> 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?
- cma 5y agoDoesn'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.
- zorgmonkey 5y agoNot 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).