Passing structs in Registers

Say we are on x64 (aka x86-64, x86_64, AMD64, Intel 64, IA32e, and EM64T). To make sure that I understand the System V psABI correctly, I compiled the following piece of C on Compiler Explorer:

#include <stdint.h>

struct A { char *p; uint32_t a; };
struct B { char *p; uint32_t a, b; };

extern fa(struct A);
extern fb(struct B);

void gaa(struct A x) { x.a = 1; fa(x); }
void gba(struct B x) { x.a = 1; fb(x); }
void gbb(struct B x) { x.b = 1; fb(x); }

This is what gcc 15.2 produces at -O2 or -O3:

gaa:
    movabs  rcx, 0xFFFF'FFFF'0000'0000
    and     rsi, rcx
    or      rsi, 1
    jmp     fa
gba:
    movabs  rcx, 0xFFFF'FFFF'0000'0000
    and     rsi, rcx
    or      rsi, 1
    jmp     fb
gbb:
    mov     rax, rsi
    mov     rsi, rdi
    mov     eax, eax
    bts     rax, 32
    mov     rdi, rax
    mov     rax, rsi
    mov     rsi, rdi
    mov     rdi, rax
    jmp     fb
And this is what clang 21.1.0 produces at -O2 or -O3:
gaa:
    mov     esi, 1
    jmp     fa@PLT

gba:
    movabs  rax, 0xFFFF'FFFF'0000'0000
    and     rsi, rax
    inc     rsi
    jmp     fb@PLT

gbb:
    mov     eax, esi
    movabs  rsi, 0x0000'0001'0000'0000
    or      rsi, rax
    jmp     fb@PLT

Both generate the same code for gba, which says a is stored in the lower half of rsi and b in the upper half. Note that clang zeroes the upper half of rsi in gaa. I find it surprising that gcc preserves it, and did not find any relevant information. I think this kind of detail is not well specified and/or documented because very few people care, which is sad.

Also, the code gcc generates for gbb seems very odd. I do not know why it shuffles registers around when it can simply do mov esi, esi; bts rsi, 32. Actually, clang does this at -Os or -Oz.

Compilers are smart yet confusing.