3 ms·
Rust's ABI needs a lot of work before it's frozen. They're leaving performance on the table. Consider this code: https://rust.godbolt.org/z/jN2AA_ https://rust
by quotemstr 7y ago
Rust's ABI needs a lot of work before it's frozen. They're leaving performance on the table. Consider this code:
https://rust.godbolt.org/z/jN2AA_ https://rust.godbolt.org/z/jN2AA_
pub fn square(num: i64) -> i64 {
return num * num;
}
// mov rax, rdi
// imul rax, rdi
// ret
pub fn square2(num: i64) -> Result<i64, Box<dyn
std::error::Error>> {
Ok(num * num)
}
// mov rax, rdi
// imul rsi, rsi
// mov qword ptr [rdi + 8], rsi
// mov qword ptr [rdi], 0
// ret
In square(), we multiply the input by itself and return the result in rax. In square2(), we call the function with an extra hidden pointer argument and store the output Result via that pointer. That's really bad considering how central this fat-return-value model is to Rust programming.
x86_64 has nine caller-saved registers. We should be using all of those to return function return values. Yes, I know that we'e returning by pointer because the Box has a non-trivial destructor. That's irrelevant, a holdover from the bad old C++ ABI. Rust should be returning from square2() via two registers, and I hope that it implements this optimization before freezing its ABI.
In C++, we can address this ABI wart using the [[clang::trivial_abi]] attribute: see https://quuxplusone.github.io/blog/2018/05/02/trivial-abi-101/ https://quuxplusone.github.io/blog/2018/05/02/trivial-abi-10...
There's no reason Rust can't do the same thing.
- ComputerGuru 7y agoYou’re completely right. But AFAIK there isn’t much of a push to cement the ABI as no one believes we’re “there” with what we have. That’s one of the things I like about rust (although a certain class of developers have a problem with that), the emphasis on trying to get things right rather than merely on time.
- zozbot234 7y ago> That's irrelevant, a holdover from the bad old C++ ABI. It's not irrelevant, it's doing whatever's most convenient given that the caller is to drop the Result<> later. As mentioned in the blogpost you link to, the [[clang::trivial_abi]] attribute changes how these things happen, such that it's no longer clear whether the caller or callee is responsible for a value that's passed via trivial_abi. I don't think it would make sense for Rust to adopt this, especially not in all cases. > That's really bad considering how central this fat-return-value model is to Rust programming. It's not 'fat return values' that lead to this, it's just boxed return values. Or more generally, values with non-trivial drop implementations.
- quotemstr 7y agoIt's not unclear in the slightest. If the value dropped before the callee returns, the callee drops it. If the value is dropped after the callee returns, the caller drops it. The claim that returning by pointer is better for a caller that's going to drop the value later makes no sense. If you think there's an actual problem, point it and be specific. The problem isn't the boxing. The problem is that the boxing triggers a needlessly inefficient calling convention, and returning boxed values is central to Rust. The language implementation as it is today uses the machine inefficiently. Rust can make up its own ABI. Returning via pointer when the architecture has tons of registers available is just bad ABI no matter what excuses people use to justify bad performance.
- manwe150 7y agoMy mental model comparison says that the cost of those may potentially be about the same, with no clear winner over many benchmarks. I’ve even seen recently a claim that using two registers was measured to be slower than stack memory return when tested in another language, so it’s been on my TODO list now to performance test it (I work on another language runtime). But I don’t actually expect to be able to measure the difference—instead I expect the latency often may simply disappear in the execution of the `ret` statement. Plus, for a non-trivial function, returning via a register may mean you have to materialize an extra load, whereas the box could have been filled much earlier and kept a tiny bit more of the stack hot.
- quotemstr 7y agoSure, that part of the stack is going to be in L1 cache, and there's not a huge difference between that and the register file. But think about the code size cost too. The icache hit won't show up in a microbenchmark, but when you have 100,000 of these functions, the difference will be real. > I’ve even seen recently a claim that using two registers was measured to be slower than stack memory return I find that difficult to believe. Those output registers are undefined anyway. I get that store-to-load forwarding may hide the latency hit of the stack access, but they'll apply to the registers too. At best, the stack pattern runs at the same speed as register return, but with a bigger code size. How on earth could a register return be worse? If it really were worse, we'd use a pointer return for multi-word PODs, and we do actually use a pair of registers to on x86_64 to return a pair of words.
- jadbox 7y agoNeat, you should definitely file a GH issue or even write up an RFC!
- The_rationalist 7y agoVery interesting, could you open an issue on rustc?