Have compiler ignoring setting an argument register before calling function

Viewed 138

TL;DR; I am looking for a standard way to basically tell the compiler to pass whatever happened to be in a given register to the next function.

Basically I have a function int bar(int a, int b, int c). In some cases c is unused and I would like to be able to call bar in the cases where c is unused without modifying rdx in any way.

For example if I have

int foo(int a, int b) { 
    int no_init; 
    return bar(a, b, no_init); 
}

I would like the assembly to just be:

For a tailcall

    jmp bar

or for a normal call

   call bar

Note: clang generally produces what I am looking for. But I am unsure if this will always be the case in more complex functions and I am hoping to not have to check the assembly each time I build.

GCC produces:

For a tailcall

    xorl %edx, %edx
    jmp bar

or for a normal call

   xorl %edx, %edx
   call bar

I can get the results I want using inline assembly i.e changing foo (for tail calls) to

int foo(int a, int b) {
    asm volatile("jmp bar" : : :);
    __builtin_unreachable();
}

which compiles to just

   jmp bar

I understand that the performance implications of an xorl %edx, %edx is about as close to 0 as possible but

I am wondering if there is a standard way to achieve this.

I.e I can probably find a hack for it for any given case. But that will require me verifying the assembly each time. I am looking for a method that you can basically tell the compiler "pass whatever happened to be in register".

See for examples: https://godbolt.org/z/eh1vK8

Edit: This is happening with -O3 set.

4 Answers

I am wondering if there is a standard way to achieve this.

I.e I can probably find a hack for it for any given case. But that will require me verifying the assembly each time. I am looking for a method that you can basically tell the compiler "pass whatever happened to be in register".

No, there is no standard way to achieve it in either C or C++. Neither of these languages speak to any lower-level function call semantics, nor even acknowledge the existence of CPU registers,* and both languages require every function call to provide arguments corresponding to all non-optional parameters (which is simply "all declared parameters" in C).

For example if I have

int foo(int a, int b) { 
    int no_init; 
    return bar(a, b, no_init); 
}

... then you reap undefined behavior as a result of using the value of no_init while it is indeterminate. Whatever any particular C or C++ implementation that accepts that at all does with it is non-standard by definition.

If you want to call bar(), but you don't care what value is passed as the third argument, then why not just choose a convenient value to pass? Zero, for example:

    return bar(a, b, 0); 

*Even the register keyword does not do this as far as either language standard is concerned.

Note that if the called function does read its 3rd arg, leaving it unwritten risks creating a false dependency on whatever last used EDX. For example it might be the result of a cache-miss load, or a long chain of calculations.

GCC is careful to xor-zero to break false dependencies in a lot of cases, e.g. before cvtsi2ss (bad ISA design) or popcnt (Sandybridge-family quirk).

Usually the xor edx,edx is basically a wasted 2-byte NOP, but it does prevent possible coupling of otherwise-independent dependency chains (critical paths).

If you're sure you want to defeat the compiler's attempt to protect you from that, then Nate's asm("" :"=r"(var)); is a good way to do an integer version of _mm_undefined_ps() that actually leaves a register uninitialized. (Note that _mm_undefined_ps doesn't guarantee leaving an XMM reg unwritten; some compilers will xor-zero one for you instead of fully implementing the false-dependency recklessness that intrinsic was designed to allow for Intel's compiler.)

Cast the function to have the smaller signature (i.e. fewer parameters):

extern int bar(int, int, int);

int foo(int a, int int b) {
    return ((int (*)(int,int))bar)(a, b); 
}

Maybe make a macro for 2 parameter bar, and even get rid of foo:

extern int bar3(int, int, int);

#define bar2(a,b) ((int (*)(int,int))bar3)(a,b)

int userOfBar(int a, int b) { return bar2 (a,b); }

https://godbolt.org/z/Gn4a69

Oddly, given the above gcc doesn't touch %edx, but clang does... oh, well.

(Still can't guarantee the compiler won't touch some registers, though, that's its domain.  Otherwise, you can write these functions directly in assembly and avoid the middleperson.)

One approach that should work for gcc/clang on most platforms is to do

    int no_init; 
    asm("" : "=r" (no_init));
    return bar(a, b, no_init); 

This way you don't have to lie to the compiler about the prototype of bar (whichc could break some calling conventions), and you fool the compiler into thinking no_init is really initialized.

I would wonder about an architecture like Itanium with its "trap bit" that causes a fault when an uninitialized register is accessed. This code would probably not be safe there.

There is no portable way to get this behavior that I know of, but you could ifdef it:

#ifdef __GNUC__
#define UNUSED_INT ({ int x; asm("" : "=r" (x)); x; })
#else
#define UNUSED_INT 0
#endif
// ...
   bar(a, b, UNUSED_INT);

Then you can fall back to the (infinitesimally) less efficient but correct code when necessary.

It results in a bare jmp on gcc/x86-64, see https://godbolt.org/z/d3ordK. On x86-32 it is not quite optimal as it pushes an uninitialized register, instead of just adjusting an existing subtraction from esp. Note that a bare jmp/call is not safe on x86-32 because that third stack slot may contain something important, and the callee is allowed to overwrite it (even if the variable is unused on the path you have in mind, the compiler could be using it as scratch space).

One portable alternative would be to rewrite bar to be variadic. However, then it would need to use va_arg to retrieve the third argument when it is present, and that tends to be less efficient.

Related