Some strange optimizations of llvm

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

Thanks a lot!