I have the simple code below
#include <cstddef>
struct X {
size_t x;
size_t y;
size_t z;
};
extern void f(X);
int main() {
f({1, 2, 3});
return 0;
}
and try to compile with -O3 -mno-sse for gcc and clang
clang:
main: # @main
sub rsp, 24
mov qword ptr [rsp], 1
mov qword ptr [rsp + 8], 2
mov qword ptr [rsp + 16], 3
sub rsp, 8
mov rax, qword ptr [rsp + 16]
push 3
push rax
push 1
call f(X)@PLT
add rsp, 32
xor eax, eax
add rsp, 24
ret
gcc:
main:
sub rsp, 48
push 3
push 2
push 1
call f(X)
xor eax, eax
add rsp, 72
ret
The code generated by clang is strange, and it seems to have some meaningless operations. Is there any special reason for this?
It seems to happen in the first run of instruction selection Compiler Explorer. For whatever reason it’s introducing a load from that address and then passing it by register into the stack pointer. No clue why it’s doing that, you’d likely need to dive into SelectionDAG to figure out where this is emitted.
This is a longstanding issue with the way LLVM represents passing large arguments on the stack. If you look at the LLVM IR, we allocate a variable, initialize it, then pass it to a “byval” argument. But “byval” itself is an implicit copy of the argument pointer, so we copy the variable into the parameter.
In this particular case, it looks like SelectionDAG manages to partially fold byval copy after we lower it, which is why the end result looks a bit weird.
In theory, we could do byval lowering earlier (along the lines of llvm.call.preallocated.setup), but it hasn’t been a priority for anyone to fix.
3 Likes
Thanks for your reply. Recently, I am researching the backend optimization. So I am interested in this problem, is it hard to resolve? Maybe I will have a try.
Writing a proof-of-concept pass to convert byval to the “preallocated” intrinsics is relatively easy: if you understand the semantics of byval and preallocated, it’s a straightforward conversion. Getting everything surrounding that working reliably is probably a large project… I’m not sure how stable preallocated actually is at the moment. (It’s currently not used in production anywhere, I think. clang currently uses inalloca, the previous iteration of the feature with a lot more restrictions.)
1 Like