I think you should define the value of the constant in a stand-alone asm or C file. This is a guaranteed way to stop the compiler inlining the value anywhere even with link-time optimization, without using anything as inefficient as volatile
. i.e. the Ada compiler never sees the value of the constant at all, in any source file.
I don't know Ada, but the C equivalent would be extern const int my_const;
and then in a separate constant.S
file use .section .rodata
/ my_const: .long 0x12345
. Or use a custom section and a linker script to get your modifiable constants placed somewhere specific in your binary.
For more about interfacing Ada with with C or asm, see https://gcc.gnu.org/onlinedocs/gcc-7.3.0/gnat_ugn/Interfacing-to-C.html. It has examples of importing and exporting symbol definitions to/from C. (And the mapping between C and asm is very simple, so you can just tell Ada that it's C even if you create the object files using asm.)
My_Num : Integer;
pragma Import (C, My_Num, "Ada_my_num");
Ideally you can declare My_Num
in a way that the Ada compiler knows it's a constant. This declares it as a plain global (with an external definition using the Ada_my_num
C symbol name.)
This is pretty similar to what @Jose's answer is suggesting, except using stand-alone asm to hide the value from the compiler.
You want the compiler to be able to optimize as much as possible. i.e. to assume that the value of this "variable" doesn't change during the lifetime of the program, so it can load it into a register at the start of a function and assume that calls to non-inline functions can't change it. Or to avoid redoing calculations involving it (CSE), so if your source code has
a * my_const
both before and after a function call, it can save the result in a register instead of reloading the constant from memory and redoing the multiply after the function call.
This can't happen if the compiler thinks it's an ordinary global variable with unknown value; it would have to assume that the function call might have changed the value of any global variable.
But if you used an ordinary global variable and assign a value to it anywhere the Ada compiler can see, then whole-program link-time optimization could propagate that value to other places, or even bake it into other constants. (e.g. if you ever do a += 2 * my_constant
, you could have 2*my_constant
hard-coded somewhere in your asm output).
(e.g. if you compile+link with -flto
to let the compiler optimize between compilation units. IDK if GNAT can do this the same way the C front-end can, but hopefully it can.)
Why the compiler does this: because it's more efficient, of course!
Loading a value from static data in memory typically takes multiple instructions to generate a 32-bit address (on a fixed instruction-width ISA like SPARC); with the same number of instructions you could have created an arbitrary 32-bit constant in a register directly. ALU instructions are typically cheaper than loads, and can't miss in cache.
Small constants are even more efficient, and can be used as a single immediate operand for add
, or
, and
, or whatever.
Constant folding and constant propagation after inlining is a major way that the asm version of a program can do less work than the source. e.g. 5 * my_const
can be done at compile-time if the compiler knows the value of my_const
. (So it could generate that in a register directly, if needed, instead of loading my_const
and using a shift/add.)
Some generic function might check if(x>0)
, but the compiler might be able to prove that's always true in one place where the function inlines, if it knows something about the values of constants. (That's a value-range optimization).
Denying your compiler the value of a constant can definitely make your code less efficient, depending on how you use the constant.
Example compiler output. (I assume you can write equivalent Ada which GNAT's / gcc's SPARC back-end will optimize similarly to what clang/LLVM -target sparc
does). The point of this is to illustrate the difference between a constant of unknown value vs. a known constant:
(From clang6.0 -O3 -fomit-frame-pointer -target sparc
on the Godbolt compiler explorer)
const int my_const1, my_const2;
static const int my_static = 123; // a small constant that works as an immediate
int foo(int x) {
return x + my_static;
}
retl
add %o0, 123, %o0 # branch-delay slot
int one_constant(int x) {
return x + my_const1;
}
sethi %hi(my_const1), %o1
ld [%o1+%lo(my_const1)], %o1
retl
add %o1, %o0, %o0
The latter is pretty obviously less efficient. It seems that clang doesn't know if / when %hi(my_const1)
and %hi(my_const2)
are the same, so it uses another sethi
for each static location. Ideally the compiler could use the same reference point for multiple accesses inside a large function, but that doesn't seem to be the case for clang/LLVM. Godbolt doesn't have SPARC gcc, so I couldn't try that easily.