Why is gcc behaving this way?
So why? Let's have a look at the cost computation in gcc/config/avr/avr.cc:avr_rtx_costs()
:
case E_HImode:
if (AVR_HAVE_MUL)
{
rtx op0 = XEXP (x, 0);
rtx op1 = XEXP (x, 1);
...
if (...)
...
else if (register_operand (op0, HImode)
&& (u8_operand (op1, HImode)
|| s8_operand (op1, HImode)))
{
*total = COSTS_N_INSNS (!speed ? 6 : 9);
return true;
}
This is the cost for multiplication of a 16-bit integral value with an 8-bit compile-time constant (signed or unsigned). The costs are:
- 9 (in unity of ticks) when compiling for speed,
- 6 (in units of words) otherwise, e.g. when compiling for size.
Such a sequence would read something like
ldi R_ten, 10 ; 1
mul R_in.lo8, R_ten ; 2
movw R_OUT, R0 ; 1
mul R_IN.hi8, R_ten ; 2
add R_OUT.hi8, R0 ; 1
clr __zero_reg__ ; 1
where the output register must not overlap with any of the input registers. The actual costs are 6 words and 8 ticks, and to me it is not completely clear why avr-gcc is using 9 ticks in the cost model. Such penalty would usually account for costs that are not visible at that stage, like high register pressure or additional moves forced by early-clobber. And indeed, the real sequence has a superfluous move instruction and actually costs 9 ticks...
Where are the costs of the shift+add sequence? They are not visible here. The costs are used during instruction selection when lowering tree-SSA to RTL. The middle-end knows some algorithms to expand mul-with-const and will add costs for the required operations: (3 * shift + 1 * add) * 2 bytes = 8 ticks.
So shift+add appears cheaper than MUL. Tree-SSA → RTL lowering takes place3 in pass 234 out of 316, and costs due to register allocation (like spills, reloads or moves) are not available because reg alloc takes place later, at pass 280 / 316.
and how can I tell it to use hardware multiplication instructions?
There is no such option. Instruction selection follows from the device familiy, which follows from -mmcu=...
. If you insist on using MUL
, you'd have to use inline assembly like for example:
__attribute__((__always_inline__))
static inline uint16_t mul_u16_u8 (uint16_t a, uint8_t b)
{
#if defined __AVR_HAVE_MUL__
uint16_t result;
__asm ("mul %A[in16], %[in8]" "\n\t"
"movw %[res], r0" "\n\t"
"mul %B[in16], %[in8]" "\n\t"
"add %B[res], r0" "\n\t"
"clr __zero_reg__"
: [res] "=&r" (result)
: [in16] "r" (a), [in8] "r" (b));
return result;
#else
return a * b;
#endif
}
uint16_t use_mul_u16_u8 (uint16_t x)
{
return mul_u16_u8 (x, 10);
}
The inline squence takes 7 ticks, and setting b = 10
takes one more tick. Assuming no other instructions are required2, it consumes 8 ticks. The final CLR
is required due to avr-gcc ABI, see this answer.
What does not work though is something like
__attribute__((__optimize__("Os"), __always_inline__))
static inline uint16_t mul_Os (uint16_t a, uint8_t b)
{
return a * b;
}
uint16_t use_mul_Os (uint16_t x)
{
return mul_Os (x, 10);
}
and compile with -O2
. Inlining takes place prior to instructio selection, and even though GCC annotates every RTL insn whether to optimize for size or speed, it doesn't work out here (dunno if that could be considered a GCC problem).
Footnotes
2For the test case, the assumption does not hold because the output operand of the asm is early clobber, and both result and 1st input will occupy R25:R24, hence at least one more MOVW
is needed.
3Using pass numbers from avr-gcc v8.