Optimization#
This page describes the options that control optimization: the -O
levels EmbCC accepts and exactly what each one turns on, the -fNAME and
-fno-NAME options that switch one optimizer pass on or off, the
assumptions the optimizer makes about a program (signed overflow,
aliasing, floating point, volatile), the --sanitize= checks, and the
ways to see what the optimizer did. It is written for people who compile
code with EmbCC and want to know what an optimization level costs and
what it promises.
Overview#
Optimization happens in two places.
- The IR optimizer (
src/opt/opt.c) rewrites EmbIR, the compiler's intermediate representation, one function at a time, after inlining. Its passes iterate until nothing changes. Each pass that has a name can be switched on or off with-fNAME/-fno-NAME. - The code generator of each target (
src/arch/TARGET/codegen.c) changes what it emits by level: register allocation, tail calls, frameless functions and function alignment are decided there. These have no individual switches.
The default is --opt=none: no IR optimization, and every source variable in
its stack slot.
Optimization levels#
| Option | Effect |
|---|---|
--opt=none |
No optimization. The default. |
-O, --opt=basic |
Register allocation, and the passes that only remove work: promotion of locals to registers, constant propagation, dead stores, redundant loads, loop invariants, if-conversion, division by constants. |
--opt=speed |
Everything in --opt=basic, plus inlining, global CSE, partial redundancy elimination, unrolling, vectorizing, and the other passes that trade size or compile time for speed. |
--opt=max |
Identical to --opt=speed. |
--opt=size |
--opt=speed without the passes that trade size for speed, a smaller inlining budget, denser jump tables, and no function alignment padding. |
--opt=min-size |
Identical to --opt=size. |
--opt=none#
GCC: -O0.
Do not optimize. This is the default.
The IR optimizer does not run, so -f pass options have no effect at
this level. The one exception is the inliner, which copies in each call to
an always_inline function and nothing else, as GCC and Clang do at
--opt=none. Every source variable -- every local and parameter --
lives in its stack slot and is stored there by every assignment, so it
is in memory at all times, which is what a debugger wants (see
Debugging).
The temporaries of an expression, which no debugger names, are where the
targets differ. On ARM Cortex-M, RISC-V and AVR the register allocator
runs for them, as GCC's and Clang's -O0 keep them in registers:
a + b * c loads a, b and c and computes in registers. On x86-64
and AArch64 every temporary has a stack slot too, and every C operation
is a separate load, operation and store. (FreeRTOS's kernel at --opt=none on a
Cortex-M3: 106678 bytes of code before, 25670 now; Clang's -O0 is
18870; lib/libc's string, stdlib and ctype on AVR, 68428 bytes before
and 33330 now.) EMBCC_O0_NORA=1 gives the old --opt=none on these targets,
for bisecting a difference. On AVR, -g still turns the allocator off.
A very big function is compiled the old way as well: one of more than
100000 IR instructions, some 12000 statements of straight-line code on
Cortex-M and RISC-V and 10000 on AVR (ra_o0_too_big in
src/arch/regalloc.c). Allocating costs time in proportion to the
function; AVR, which generates each function several times to keep the
shortest, takes about a second for 6000 statements, where Cortex-M
takes a tenth of one.
Some decisions are made at every level, --opt=none included, because they are
taken when the IR is first generated or when code is emitted:
switchstatements are lowered to a jump table or a tree of compares by the rules in Switch lowering.- A
staticfunction that nothing calls is not emitted. - On x86-64 and AArch64, functions start on a 16-byte boundary (see Function alignment).
- On x86-64, temporaries that are not live at the same time share stack slots within a basic block.
- Branches are shortened to their smallest encoding.
-O, --opt=basic#
GCC: -O1.
Optimize without making the code larger. This is the level of GCC's and
Clang's -O1. -O is the same as -O1.
Values live in registers: every code generator allocates registers, and
mem2reg and sroa take local variables out of memory. The passes that
only remove work run: the local passes listed in
Passes with no switch, cfg-clean, sccp,
dse, load-cse, licm, if-convert and div-magic, with attribute
inference and constant propagation from read-only static variables.
Inlining at --opt=basic is limited to what makes code smaller or was asked
for: a static function with a single caller (whose original is then
deleted) and an always_inline function. The other passes that trade
size or compile time for speed wait for --opt=speed: gcse, pre, unroll,
vectorize, switch-thread, tail-recursion and idiom. Code at
--opt=basic is usually a little smaller than at --opt=speed.
After optimization, a static function that no longer has a reachable
caller is not emitted. This is decided by reachability from the
functions visible outside the unit, constructors, destructors and
functions marked __attribute__((used)), so a static function called
only from another dead static function is dropped as well.
The code generators do at --opt=basic everything they do at --opt=speed; see
Code generation by level.
--opt=speed#
GCC: -O2.
Optimize for speed. Enables everything in --opt=basic, and:
- every other pass in the pass table,
except
vectorizeon targets other than x86-64; - function inlining, with the budgets in Inlining.
--opt=max#
GCC: -O3.
Accepted, and identical to --opt=speed. No pass or code-generation decision in
EmbCC tests for a level above 2, and objects built with --opt=max are byte
for byte those built with --opt=speed.
--opt=size#
GCC: -Os.
Optimize for size. Enables everything in --opt=speed except:
vectorize,unrollandswitch-threadare off, and so is live-range splitting around loops. Each of these adds code.- The inlining budget for a callee that is copied is 6 IR instructions
instead of 24, and a
staticfunction with two callers gets no larger budget (200 at--opt=speed). Astaticfunction with a single caller is still inlined up to 2000 IR instructions, because moving it deletes the original (see Inlining). - A
switchneeds at least six cases and a value range of at most twice the number of cases to get a jump table (see Switch lowering). - Functions are not padded to 16 bytes on x86-64 or AArch64.
Register allocation and tail calls are the same as at --opt=speed.
--opt=min-size#
GCC: -Oz.
Accepted, and identical to --opt=size.
--opt=debug#
GCC: -Og.
Optimize for debugging: --opt=basic, with every source variable kept in its
stack slot, where a debugger reads it. See
Debugging.
-Ofast#
The same as -O3. GCC's -Ofast adds -ffast-math, which EmbCC does
not do.
Spellings EmbCC does not accept#
Any level above 3 is refused:
embcc: unknown optimization flag '-O4'
Combining -O options#
When more than one -O option is given, the last one sets the level,
with one exception: once --opt=size or --opt=min-size has appeared, a later --opt=none,
--opt=basic, --opt=speed or --opt=max does not cancel the size
request. --opt=size --opt=speed compiles as --opt=size, and
--opt=size --opt=none runs the IR optimizer at its --opt=size setting
while the code generator works at level 0.
The pass options described below are independent of their position on
the command line: -fno-sccp -O2 and -O2 -fno-sccp are the same.
What each level enables#
--opt=none |
--opt=basic |
--opt=speed |
--opt=size |
|
|---|---|---|---|---|
| Local passes (no switch) | yes | yes | yes | |
cfg-clean |
yes | yes | yes | |
Unreachable static functions dropped after optimization |
yes | yes | yes | |
mem2reg, sroa, load-cse, sccp, dse |
yes | yes | yes | |
licm (rotation, invariant motion, strength reduction) |
yes | yes | yes | |
Live-range splitting around loops (part of licm) |
yes | yes | ||
div-magic, if-convert |
yes | yes | yes | |
Attribute inference, read-only static propagation |
yes | yes | yes | |
| Register allocation, tail calls (code generator) | yes | yes | yes | |
gcse, pre, tail-recursion, idiom |
yes | yes | ||
inline |
always_inline only |
single caller or always_inline |
yes | yes, budget 6 |
vectorize |
x86-64 only | |||
unroll, switch-thread |
yes | |||
| 16-byte function alignment (x86-64, AArch64) | yes | yes | yes |
Controlling individual passes#
--pass=NAME, --no-pass=NAME#
GCC: -fNAME, -fno-NAME.
Turn the optimizer pass NAME on or off. NAME is one of the
eighteen names below. The level sets each pass's default, and an
explicit option overrides the level wherever it appears on the command
line.
embcc --opt=speed --no-pass=inline -c foo.c # --opt=speed without inlining
embcc --opt=basic --pass=gcse -c foo.c # --opt=basic and one --opt=speed pass
A pass option takes effect only when the IR optimizer runs, which is at
--opt=basic and above. At --opt=none it is accepted and does nothing.
--pass=NAME at --opt=basic runs that pass at --opt=basic. Nothing
else changes: the inlining budgets stay --opt=basic's.
A name that is not a pass is refused, with the list:
embcc: error: --pass=bogus: no optimizer pass has that name; the passes are mem2reg, gcse, load-cse, sccp, licm, vectorize, inline, dse, div-magic, if-convert, cfg-clean, tail-recursion, idiom, sroa, unroll, pre, switch-thread and licm-mem
GCC's own names for some of these passes
are read too, and some are not (-ftree-vectorize); see
GCC's optimization options.
Turning passes off one at a time is the quickest way to find which one changes the behavior of a program:
for p in inline sroa licm unroll vectorize pre; do
embcc -O2 -fno-$p prog.c -o prog && ./prog > out.$p
done
The table lists every pass. The entries that follow describe each one.
| Pass | Default | What it does |
|---|---|---|
mem2reg |
--opt=basic, --opt=speed, --opt=size |
Promote scalar locals out of memory |
gcse |
--opt=speed, --opt=size |
Common subexpressions across blocks |
load-cse |
--opt=basic, --opt=speed, --opt=size |
Redundant loads across blocks |
sccp |
--opt=basic, --opt=speed, --opt=size |
Constant branches and unreachable blocks |
licm |
--opt=basic, --opt=speed, --opt=size |
Loop rotation, invariant motion, induction-variable strength reduction |
vectorize |
--opt=speed, x86-64 only |
Lane-wise loops (SSE2) |
inline |
--opt=basic (limited), --opt=speed, --opt=size |
Function inlining |
dse |
--opt=basic, --opt=speed, --opt=size |
Stores overwritten before they are read |
div-magic |
--opt=basic, --opt=speed, --opt=size |
Division by a constant without a divide |
if-convert |
--opt=basic, --opt=speed, --opt=size |
A two-way choice without a branch |
cfg-clean |
--opt=basic, --opt=speed, --opt=size |
Jump threading and dead jumps |
tail-recursion |
--opt=speed, --opt=size |
A self tail call becomes a loop |
idiom |
--opt=speed, --opt=size |
Copy and clear loops become block operations |
sroa |
--opt=basic, --opt=speed, --opt=size |
Split a local aggregate into scalars |
unroll |
--opt=speed |
Loop unrolling |
pre |
--opt=speed, --opt=size |
Partial redundancy elimination |
switch-thread |
--opt=speed |
State machines jump straight to the next case |
licm-mem |
--opt=basic, --opt=speed, --opt=size |
Loads out of loops; a variable in memory kept in a register across one |
Two kinds of function get less than the full pipeline at any level:
- A function that uses computed
goto(goto *p) is not given the passes that insert code on a control-flow edge:mem2reg,licm,vectorize,if-convert,unroll,switch-thread,pre,tail-recursion,idiomand the secondsroaround. Its locals stay in memory. - A C++ function with an exception region (a
tryblock, or an object whose destructor must run during unwinding) gets only the block-local passes,sroa,div-magicandcfg-clean. Once no call in a region can throw, the region is removed and the function is optimized normally.
--pass=mem2reg, --no-pass=mem2reg#
GCC: -fmem2reg, -fno-mem2reg.
Promote every scalar local variable whose address is never taken out of
its stack slot and into SSA values, which the later passes and the
register allocator work on. A variable live across a loop or down one arm
of an if becomes a value like any other.
A long double local is promoted too, as a 16-byte value, whether it is
x87 extended or IEEE binary128. Kept in memory, every read of one was a
16-byte copy, and on the 32-bit binary128 targets that was most of a
long double function's code. Promoting them made RISC-V's lib/libc
3% smaller.
A volatile local is never promoted. A local whose address is taken
stays in memory, unless sroa first shows that the address is used only
for loads and stores at constant offsets.
This pass is what makes most of the optimizer effective. With --no-pass=mem2reg,
locals stay in memory and the other global passes see little. Default:
on at --opt=basic, --opt=speed and --opt=size.
--pass=gcse, --no-pass=gcse#
GCC: -fgcse, -fno-gcse.
Global common-subexpression elimination. An integer computation already
made in a block that dominates the current one is reused instead of
being computed again. Arithmetic, shifts and comparisons are reused, and
so is the address of a global variable or a string literal, which takes
two instructions on Thumb, RISC-V and AArch64 (except on AVR, where
keeping it live costs more than rebuilding it). A constant or a local's
address is cheaper to recompute than to keep live, and memory reads are
left to load-cse. Floating-point operations are never reused.
EMBCC_NO_GCSE_ADDR=1 stops the reuse of addresses. Default: on at
--opt=speed and --opt=size.
--pass=load-cse, --no-pass=load-cse#
GCC: -fload-cse, -fno-load-cse.
Remove a load that repeats an earlier load of the same address on
every path to it, with no store, call, atomic operation, fence or inline
asm in between that could change the value. The second load becomes a
copy of the first. A store counts only if it can reach the bytes the load
read: one through the same pointer at a constant offset whose bytes do
not overlap them does not.
unsigned n = q->len;
q->wr = q->head; /* other bytes of *q: q->len is not reloaded */
return n + q->len;
volatile loads are never removed, and a volatile store still counts
for anything in the same object. Default: on at --opt=basic, --opt=speed and --opt=size.
--pass=sccp, --no-pass=sccp#
GCC: -fsccp, -fno-sccp.
Resolve a conditional branch whose condition is a known constant into a
jump, and delete every block that is then unreachable. The constants
typically come from inlining a call with a constant argument, or from a
configuration macro. Default: on at --opt=basic, --opt=speed and --opt=size.
--pass=licm, --no-pass=licm#
GCC: -flicm, -fno-licm.
The loop passes:
- Rotation. A loop is turned from top-tested into bottom-tested, so each iteration runs one conditional branch instead of a branch and a jump. The test is duplicated as a guard before the loop. When the loop's start and bound are both constants, the guard is decided at compile time and does not appear in the output.
- Loop-invariant code motion. A computation whose operands do not change in the loop is moved in front of it. Only operations that cannot fault are moved, so a load through a pointer, a division and a remainder stay in the loop. A read of a local variable is moved only when nothing in the loop writes it and its address is never taken.
- Memory in loops (
--no-pass=licm-memturns off just this). A load whose address does not change is moved in front of the loop when nothing in the loop can write those bytes -- no store that may reach them, no call that may write memory, and no inline asm, atomic or volatile access anywhere in the loop -- and when moving it cannot fault: the loop would have executed it anyway, or it reads a global or a local inside its bounds. A location the loop both writes and reads, such asp->count++or a global sum, is kept in a register instead when nothing else in the loop can reach it, the loop calls nothing that touches memory, and every way out of the loop has just stored it; it is stored once on each way out.volatileand_Atomicobjects are never moved or kept, and nothing moves across an access to one. - Induction-variable strength reduction. An array access
a[i]in a loop is turned into a pointer that advances by the element size each iteration. - Live-range splitting (not at
--opt=size). A value that is live across a call outside a loop, but not across any call inside it, gets a second name inside the loop, so the loop does not keep it in a callee-saved register.
Default: on at --opt=basic, --opt=speed and --opt=size (live-range splitting at --opt=basic and --opt=speed only).
--pass=vectorize, --no-pass=vectorize#
GCC: -fvectorize, -fno-vectorize.
Rewrite a loop to process 16 bytes per iteration with SSE2 vector
instructions. Only the x86-64 code generator has vector instructions, so
the default is on at --opt=speed for x86-64 and off everywhere else. Forcing it
on for another target with --pass=vectorize stops the compilation as soon as
a loop is vectorized, with an error such as:
embcc: v.c:3: error: aarch64 codegen: unhandled IR op 56 in 'vi'
embcc: v.c:3: error: the ARMv7-M backend cannot lower this operation at 64 bits yet (function vi) [vload w=8 size=4]
A loop is vectorized when all of the following hold:
- it counts an induction variable up from 0 by 1, to a constant or to a value computed before the loop (a constant count must be a multiple of the lane count; a run-time count leaves a scalar loop for the remainder);
- every memory access is
array[i]on a global array, all with the same element size of 4 or 8 bytes, so no two accesses can overlap; - no access is
volatile; - every operation is an add, subtract, and, or, xor, a shift by a constant, or a multiply by a constant of the form 2^k or 2^k + 1;
- or the loop is an integer sum (
s += a[i]), which is kept as four partial sums and added up after the loop.
A loop over arrays reached through a pointer is not vectorized.
Default: on at --opt=speed for x86-64. Off at --opt=size.
A loop over float or double arrays is not vectorized. --remarks
reports each loop that is.
--pass=inline, --no-pass=inline#
GCC: -finline, -fno-inline.
Replace a call to a small function defined in the same translation unit
with the function's body. See Inlining for the budgets and
for what is never inlined. Default: on at --opt=speed and --opt=size, with a smaller
budget at --opt=size; at --opt=basic, only for a static function with a single
caller and for an always_inline function.
--pass=dse, --no-pass=dse#
GCC: -fdse, -fno-dse.
Dead-store elimination. Within a basic block, a store is removed when a later store writes the same address at the same width and nothing in between can read it:
p->x = 1; /* removed */
p->x = 2;
The later store must write every byte of the earlier one through the
same pointer value: the same field, or a wider store over a narrower one
(((char *)p)[1] = 5; *p = 7; loses the byte store). A read in between
keeps the earlier store unless it reads other bytes of the same pointer
or a different object. A volatile store is never removed. (A store to a
local that is never read at all is removed by dead-code elimination at
--opt=basic.) Default: on at --opt=basic, --opt=speed and --opt=size.
--pass=div-magic, --no-pass=div-magic#
GCC: -fdiv-magic, -fno-div-magic.
Replace a 32-bit division or remainder by a constant with a
multiplication by a "magic" constant and shifts, signed or unsigned.
64-bit divisions are left alone, because the 128-bit product they would
need is a library call on most targets. Unsigned division by a power of
two is already a shift at --opt=basic. Default: on at --opt=basic, --opt=speed and --opt=size, because
the multiply sequence is also smaller than a divide on these targets.
--pass=if-convert, --no-pass=if-convert#
GCC: -fif-convert, -fno-if-convert.
Turn a branch whose two arms each only copy an already computed value into a select:
x = c ? a : b; /* no branch */
A select evaluates both arms, so only arms that are plain values are
converted; an arm with a load, a call or anything that can fault keeps
its branch. On x86-64 a select is a cmov, on AArch64 a csel; the other
code generators lower it in their own way. Default: on at --opt=basic, --opt=speed
and --opt=size.
--pass=cfg-clean, --no-pass=cfg-clean#
GCC: -fcfg-clean, -fno-cfg-clean.
Simplify the control-flow graph:
- a jump to a jump goes straight to the final target;
- a branch whose two targets are the same becomes a jump;
- a jump to the next instruction is removed;
- the 0/1 value built for
&&and||in a condition is not built, merged and tested again; each arm jumps to where it is going; - unreachable code is removed.
Default: on at --opt=basic, --opt=speed and --opt=size.
--pass=tail-recursion, --no-pass=tail-recursion#
GCC: -ftail-recursion, -fno-tail-recursion.
Turn return f(...), where f is the function itself, into an
assignment of the new arguments and a jump to the top of the function.
The recursion becomes a loop that uses no stack and that the loop
passes can optimize. It is not done when the address of a parameter is
taken. Calls to other functions in tail position are handled by the code
generator (see Code generation by level).
Default: on at --opt=speed and --opt=size.
--pass=idiom, --no-pass=idiom#
GCC: -fidiom, -fno-idiom.
Recognize a loop with a constant trip count that does nothing but clear an array or copy one array to another, and replace it with a single block clear or block copy:
for (i = 0; i < 64; i++) a[i] = 0; /* one 256-byte clear */
for (i = 0; i < 64; i++) a[i] = b[i]; /* one 256-byte copy */
For a copy, both arrays must be distinct global objects. The block
operation is expanded inline by every code generator; no call to
memset or memcpy is introduced (x86-64 uses rep movsq and
rep stosq for blocks over 256 bytes). Default: on at --opt=speed and --opt=size.
--pass=sroa, --no-pass=sroa#
GCC: -fsroa, -fno-sroa.
Scalar replacement of aggregates. A local struct, union or array
whose address is used only for loads and stores at constant offsets is
split into one independent scalar per field accessed, which mem2reg
then promotes to registers. The aggregate must be at most 128 bytes and
split into at most 16 pieces.
It is not split when it is volatile, when any access is volatile,
when it is indexed by a variable, when its address is passed to a
function or stored, or when two accesses overlap with different widths
(a union used to reinterpret bytes). Default: on at --opt=basic, --opt=speed and --opt=size.
--pass=unroll, --no-pass=unroll#
GCC: -funroll, -fno-unroll.
Copy the body of a counted loop several times with one test between
groups of copies. A loop is unrolled when it is bottom-tested (after
rotation), steps its induction variable by 1, compares it with a signed
< against a loop-invariant bound, has no other exit, and contains no
alloca, variable-length array, inline asm or computed goto.
| Body size (IR instructions) | Copies |
|---|---|
| 1 to 12 | 8 |
| 13 to 20 | 4 |
| more than 20 | not unrolled |
The number of copies is the largest power of two, up to 8, for which
copies times body is at most 96. The original loop is kept unchanged after the unrolled one and runs the
remaining iterations. Default: on at --opt=speed; off at --opt=size.
A loop whose trip count is a constant is copied whole instead, and no
loop is left, when the count is at most 32 and the copies come to at
most 200 IR instructions. The count is known when the loop counts from
one constant to another (for (i = 3; i < 11; i++)), or walks a pointer
from one constant offset of a base to another. A body that calls a
runtime helper (soft-float arithmetic, for example) is not copied
whole.
A loop whose trip count cannot be known -- a list walk, a search, a scan
for a terminator, while (*pp && (*pp)->key < key) pp = &(*pp)->next;
-- is copied four times when its body is at most six IR instructions,
each copy keeping its own exits and only the last branching back. The
values the loop carries are renamed from copy to copy, so a pointer
chase needs no register copy per element, and an exit from an earlier
copy goes through a few moves after the loop that put them back. Code
that leaves the loop from its middle (if (*p == c) return p;) is
first moved out of the loop's way. --remarks names each such loop
unroll/exits-kept; EMBCC_NO_UNROLL_EXITS=1 turns this off alone.
--pass=pre, --no-pass=pre#
GCC: -fpre, -fno-pre.
Partial redundancy elimination. When an expression is computed on some paths to a point and computed again at that point, it is computed on the paths that lacked it instead, and the later computation becomes a copy:
if (c) sink(a * b);
return a * b; /* a * b is computed once on each path */
The insertion never makes any path compute the expression more often
than before. Default: on at --opt=speed and --opt=size.
--pass=switch-thread, --no-pass=switch-thread#
GCC: -fswitch-thread, -fno-switch-thread.
In a loop around a switch on a state variable, where an arm sets the
state to a constant, jump from the end of that arm straight to the case
the next iteration will take, instead of going back through the switch.
The code between the arm and the switch (the loop's increment and exit
tests) is copied once for each such case. This is the shape of a lexer,
a protocol parser or a hand-written state machine. At most 48 blocks are
copied per switch and 400 IR instructions added per function. Default:
on at --opt=speed; off at --opt=size.
Passes with no switch#
These run whenever the IR optimizer runs (--opt=basic and above), and cannot
be turned off individually:
- Constant folding of integer operations at their width and
signedness, of
floatanddoublearithmetic and comparisons on constants (see Floating point), and integer algebraic identities (x + 0,x * 1,x * 0,x & 0, ...). - Strength reduction: multiplication by a power of two becomes a shift; unsigned division and remainder by a power of two become a shift and a mask. Signed division is never a plain shift, because it rounds toward zero.
- Reassociation of constants:
(x + 1) + 2becomesx + 3. Integer only. - Local value numbering: an integer computation, or a load with no
store, call, fence or
asmsince the identical one, is reused within a basic block. A store to other bytes of the same pointer (another field, at a constant offset), to another object, or to a local whose address is never taken does not count.volatileaccesses and floating-point operations are never reused, and avolatilestore counts for every load. - Quotient and remainder:
a / banda % bwith the same operands in one block divide once; the remainder isa - q * b. - Copy propagation and dead-code elimination. A read of a local
variable whose value is unused is removed, unless the variable is
volatile. Loads through a pointer, divisions (which can trap), stores and branches are never treated as dead code, and neither are calls, except that at--opt=speedan unused call to a function found to touch no memory and not to throw is removed. - Store-to-load forwarding for local variables that are not
volatileand whose address is not taken, within a basic block, and for a privateunionwhose words are read back after a store. - Range checks:
c >= 'a' && c <= 'z', and the negated form, become one subtraction and one unsigned compare. - Instruction-selection preparation: constants become immediate operands, and constants and addresses are placed next to their use.
At --opt=speed and --opt=size these also run:
- Attribute inference. Each function is analyzed for whether it
writes memory its caller can see, or reads any. A call to a function
that does neither does not invalidate loaded values. The attributes
pureandconstare not needed for this, and are accepted without effect. - Read-only
staticvariables. Astaticvariable with file scope whose address is used only to load from it, in every function of the unit, is treated as a constant: each load of it becomes its initial value. A variable that isvolatile, thread-local, weak, in a named section, pointed to by an initializer, or in a unit with top-levelasmis excluded.
Inlining#
The inliner runs at --opt=basic and above, before the per-function passes. At
--opt=none it runs too, for always_inline functions only (the reason code
for any other call is not-always_inline-at-O0). At
--opt=basic it inlines only a static function with a single caller and an
always_inline function (the reason code for any other call is
not-a-sole-callee-at-O1). Sizes are counted in EmbIR instructions, as
embcc inspect ir prints them, before the callee is optimized.
| Limit | Value |
|---|---|
Largest callee copied into a caller, --opt=speed |
24 IR instructions |
Largest callee copied into a caller, --opt=size |
6 IR instructions |
Largest static callee with exactly one call site and its address never taken (it is moved, not copied) |
2000 IR instructions, at every level that inlines |
Largest static callee with exactly two call sites and its address never taken, --opt=speed |
200 IR instructions |
| A caller stops growing at | 800 IR instructions |
| Inlined calls per caller, at most | 64 |
Only direct calls to functions defined in the same translation unit are
inlined. The inline keyword has no influence on the decision.
__attribute__((always_inline)) lifts the size limit and nothing else:
a call that cannot be inlined for one of the reasons below is still not
inlined. __attribute__((noinline)) prevents inlining. Both are
honoured only where the inliner runs.
A call is never inlined when:
- it is indirect, or returns a structure;
- it is a call from a function to itself (a recursive function can still be inlined into its other callers, within the limits above);
- the callee is variadic or has a C++ exception region;
- the callee returns a structure,
_Complex,long doubleor__int128, or uses__int128; - a parameter is not a scalar of at most 8 bytes;
- the callee contains
va_start,allocaor a variable-length array, computedgoto, or a call to a function that returns a structure; - the caller has a C++ exception region or uses
__int128.
--remarks reports every decision with its reason, for example:
big.c:2: remark: not-inlined 'helper': 45 instructions, budget 24 [inline/callee-too-large]
big.c:5: remark: inlined 'ai': 45 instructions into c, budget 24 [inline/always_inline]
big.c:7: remark: not-inlined 'ni' [inline/callee-is-noinline]
embcc why not-inlined NAME FILE -O2 prints the same answer for one
function. See Optimization remarks.
Switch lowering#
A switch is lowered when the IR is generated, so the rule applies at
every level, --opt=none included. A jump table is used when all of these
hold:
| Condition | --opt=none to --opt=max |
--opt=size, --opt=min-size |
|---|---|---|
| Number of cases | at least 4 | at least 6 |
| Span from lowest to highest case value | at most 4 × cases + 4 | at most 2 × cases |
| Span, absolute limit | 4096 | 4096 |
| Controlling expression | no wider than a pointer | no wider than a pointer |
AVR never uses a jump table. Otherwise the cases become a binary search on the case values, ending in a chain of up to four equality tests.
Code generation by level#
The code generators read one fact: whether the level is at least 1.
--opt=basic to --opt=max, --opt=size and --opt=min-size generate code the same way; only function
alignment checks for --opt=size.
At --opt=basic and above, every code generator:
- allocates registers to values, including callee-saved registers,
which are then saved and restored. On x86-64 and AArch64, a function
with computed
gotoor a C++ exception region is compiled without register allocation; on the other targets one with computedgotois allocated like any other; - folds a constant offset into a load or store addressing mode;
- makes tail calls: a call immediately followed by a return of its value becomes a jump, under the conditions in the table below.
| Target | Specifics at --opt=basic and above |
Tail calls (--opt=basic and above) |
|---|---|---|
| x86-64 | A leaf function with no stack frame omits push rbp; an ELF function that only saves registers uses pushes and no frame pointer; a shared epilogue for functions with several returns. Frameless and push-only functions are not made with -g. |
Direct calls; no structure return; caller not variadic, no alloca, no computed goto, no exception region, no address of a local taken; all arguments in registers; not for the Windows convention. |
| AArch64 | A leaf function with no stack frame omits the x29/x30 frame record (not with -g). |
Direct, non-variadic calls; result at most 8 bytes, not a structure or floating point; function has a single return; no alloca, no exception region, no address of a local taken; all arguments in registers. Not with -g. |
| Thumb | Each function is generated with and without 64-bit register pairs and the shorter is kept (not with an FPU, not with -g); 16-bit branch and cbz forms. |
As AArch64, result at most 4 bytes, not in a VFP register; only in a function that pushes nothing. Not with -g. |
| RISC-V | RV32: each function is generated with and without register pairs and the shorter is kept; compressed branches and jumps. | As AArch64, result at most XLEN. Not with -g. |
| AVR | Nine allocation strategies are tried and the shortest code is kept. Not with -g, and not in an interrupt handler. |
Not in an interrupt handler, not with -g; result at most 8 bytes; no argument in a register the epilogue restores. |
On AVR, -g at --opt=basic and above turns register allocation off entirely. See
Debugging for every way
-g changes code.
Function alignment#
| Target | --opt=none to --opt=max |
--opt=size, --opt=min-size |
|---|---|---|
| x86-64 | 16 bytes, padded with nop |
none |
| AArch64 | 16 bytes, padded with nop |
4 bytes (instruction size) |
| Thumb | 2 bytes; 4 if the function contains inline asm |
same |
| RISC-V | 4 bytes; 2 with the C extension | same |
| AVR | 2 bytes (instruction size) | same |
Frame pointer#
--no-frame-pointer and --frame-pointer are accepted and
have no effect. What each target does:
| Target | Frame pointer |
|---|---|
| x86-64 | rbp at --opt=none. From --opt=basic, omitted in frameless leaf functions and push-only frames, unless -g. |
| AArch64 | x29 frame record at --opt=none. From --opt=basic, omitted in frameless leaf functions, unless -g. |
| Thumb | None. r7 is set up only in a function with a variable-length array or alloca. |
| RISC-V | None. s0 is set up only in a function with a variable-length array or alloca. |
| AVR | Y (r28:r29), only when the function has a stack frame, stack arguments, variable arguments, a structure return, or is an interrupt handler. |
What the optimizer assumes#
Signed integer overflow#
Signed integer arithmetic wraps in two's complement, at every level.
EmbCC behaves as if --wrap-overflow were always given: no optimization assumes
that signed overflow cannot happen. For example, x + 1 > x is not
folded to 1, and (x * 2) / 2 is not folded to x.
--wrap-overflow is accepted and changes nothing. -fno-wrapv, -ftrapv,
-fstrict-overflow and -fno-strict-overflow are not accepted
(unknown argument). To find signed overflow at run time, use
--sanitize=signed-integer-overflow.
Unsigned arithmetic wraps, as C requires. Out-of-range conversions from floating point to integer are not folded at compile time.
Arithmetic wraps at the width of its type inside an expression as well as
when the result is stored. On AVR, where int is 16 bits, an int or
unsigned int result that leaves its 16-bit range wraps before the next
operation reads it: long f(int x) { return x + 1; } returns -32768 for
32767, and (0xffffu + 1) / 2 is 0.
Aliasing#
EmbCC does no type-based alias analysis. A store through an int * is
assumed to be able to change a float that is later read, exactly as
with --no-strict-aliasing.
It does rely on C's object model: a pointer computed from one object
(&a[i], p->field, base + offset) is assumed to point into that
object and not into another. Two accesses based on different global
objects, or on different local variables, are assumed not to overlap,
and a pointer of unknown origin is assumed not to reach a local variable
whose address was never taken. Code that reaches one object by pointer
arithmetic from another is outside this assumption.
Within one object, two accesses through the same pointer value at
constant offsets (p->a and p->b, p[1] and p[3]) are assumed not
to overlap only when their byte ranges do not; the offsets are taken
modulo the address width, so this holds for any value of p. Two
different pointers into the same object are always treated as possibly
overlapping, whatever their types.
restrict is accepted and not used by the optimizer.
__attribute__((may_alias)) is accepted and has no effect, because
nothing it would disable is done. --strict-aliasing and
--no-strict-aliasing are accepted and have no effect.
Floating point#
From --opt=basic up, the optimizer evaluates float and double arithmetic
whose operands are constants, and otherwise leaves a floating-point
computation as written:
+,-,*,/, negation and the comparisons on constant operands are evaluated in the operation's own format with one rounding, to nearest, which is what the machine computes under the default rounding mode:251 / 255.0fand0.1 + 0.2become constants. Wheredoubleis binary32, as on AVR, adoubleoperation is evaluated in binary32. A floating-point division by zero folds to the infinity the machine would produce. An operation is left to run time when an operand or the result is a NaN, because which NaN an invalid operation produces differs between machines.long doublearithmetic is never evaluated at compile time;- floating-point arithmetic is not reassociated or simplified (
x + 0.0andx * 1.0stay), and two identical floating-point operations are not merged; - a multiply and an add are never contracted into a fused multiply-add;
EmbCC emits no fused multiply-add instruction on any target
(
__builtin_fmais not a builtin;fmais the library function); - negation flips the sign bit, which is exact for zero and NaN.
The conversion of a constant (for example (double)1000000) is also
evaluated, with round-to-nearest. A conversion is left to run time when
the value is not finite, when it is out of range for an integer result,
or when it would carry a NaN between float and double.
The evaluation assumes the default rounding mode and floating-point
environment, so a program that changes the rounding mode with fesetround
does not see it applied to arithmetic on constants.
--fast-math and --fp-contract=STYLE are accepted and change nothing:
EmbCC never relaxes IEEE arithmetic and never fuses a multiply and an add
(Invoking). #pragma STDC FP_CONTRACT and
#pragma STDC FENV_ACCESS are accepted without a diagnostic and have no
effect.
volatile#
Every access to a volatile object is performed as written, at every
level: it is not removed, merged, moved out of a loop, vectorized or
replaced by a value already known. This holds for an access through a
pointer, for a variable with static storage duration, and for a local
variable:
- A
volatilelocal variable is kept in its stack slot by every code generator; it is never promoted to a register or given one by the register allocator.volatile int v = 5; return v + v;readsvtwice, and(void)v;reads it once. Avolatilelocal changed betweensetjmpandlongjmpkeeps its new value, as C11 7.13.2.1 requires. - A
volatileaggregate is not split. - A
volatilebit-field is read and written through its storage unit, and every such access is volatile. Reading the field loads the unit once; assigning to it loads the unit once and stores it once. The value of the assignment expression is the assigned value converted to the field's width, not a second read of the unit.
Library calls#
The optimizer does not introduce calls to library functions. Block
copies and clears (from structure assignment or from the
idiom pass) are expanded inline. EmbCC
recognizes no library function name as a builtin, so --no-builtins and
--freestanding are accepted and change nothing here. The code
generators do call the compiler runtime for operations the target lacks
(for example software floating point, or 64-bit division on 32-bit and
8-bit targets); see Libraries.
Run-time checks: --sanitize#
EmbCC's sanitizer inserts checks that trap: a failed check executes the target's trap instruction, with no runtime library and no message. Under a debugger the program stops at the failing operation; without one it stops instead of continuing with a wrong value.
| Target | Trap instruction |
|---|---|
| x86-64 | ud2 |
| AArch64 | udf |
| Thumb | udf #0 |
| RISC-V | unimp |
| AVR | a branch to itself (AVR has no trapping instruction) |
--sanitize=CHECKS#
GCC: -fsanitize=CHECKS.
Enable the checks named in the comma-separated list CHECKS. Off by
default.
| Check | Traps when |
|---|---|
signed-integer-overflow |
a signed +, -, * or unary - (including compound assignment, ++ and --) overflows |
integer-divide-by-zero |
the divisor of / or % is zero, or a signed division is MIN / -1 |
shift, shift-exponent |
a shift count is negative, or not less than the width of the promoted left operand |
undefined |
all of the above |
Any other name is refused, by name:
embcc: error: -fsanitize=address is not supported: EmbCC's sanitizer inserts checks that TRAP, and this one needs a runtime library to report through. The ones it has are undefined, signed-integer-overflow, integer-divide-by-zero and shift
This includes --sanitize=all, null, alignment, bounds,
shift-base and float-divide-by-zero.
The checks are ordinary IR, so the optimizer removes the ones it can
prove never fail: at --opt=speed a division by a non-zero constant carries no
check, and a shift by a constant in range carries none. At --opt=none and
--opt=basic every check stays.
--no-sanitize=CHECKS#
GCC: -fno-sanitize=CHECKS.
Remove the named checks from the set enabled so far. The same names are accepted.
-fsanitize-trap, -fsanitize-trap=CHECKS, -fsanitize-undefined-trap-on-error#
Accepted for compatibility; trapping is the only mode. Note that
-fsanitize-trap=CHECKS also enables the named checks, unlike in
GCC and Clang, where it only selects how checks enabled by
--sanitize= report. -fsanitize-trap and
-fsanitize-undefined-trap-on-error without a list enable nothing.
Other options that start with -fsanitize, such as
-fsanitize-recover=all, are refused:
embcc: error: -fsanitize-recover=all is not supported; EmbCC would emit ordinary code and the flag's promise would not hold
Seeing what the optimizer did#
embcc inspect ir FILE [OPTIONS]#
Print the EmbIR of every function as it stands after optimization at the
level given in OPTIONS. At --opt=none this is the IR before any pass, so
comparing the two shows exactly what the optimizer changed:
embcc inspect ir prog.c --opt=none > before.ir
embcc inspect ir prog.c --opt=speed > after.ir
diff before.ir after.ir
Pass options apply, so embcc inspect ir prog.c -O2 -fno-licm shows the
effect of one pass. Each instruction ends with a comment giving the
source line, and usually the column, it came from. For example:
func @g nparams=2 nvars=4 vregs=29 labels=3 {
%7 = cmp.4s gt %0, #3 ; 4:9
brz.4s %7 -> L0 ; 4:5
%25 = mul.4s %0, %0 ; 1:31
...
Here mul came from line 1, the body of an inlined function. The IR is
described in EmbIR.
embcc inspect cfg FILE [OPTIONS]#
Print each function's control-flow graph as the passes see it: blocks, predecessors and successors, immediate dominators, and back edges (loops).
embcc inspect callgraph FILE [OPTIONS]#
Print which function calls which, after optimization. At --opt=speed a callee
that was inlined everywhere no longer appears.
--remarks, --remarks=json, embcc why#
GCC: -fremarks, -fremarks=json.
Report each optimization decision (inlined or not and why, variables promoted to registers, loops rotated, hoisted, vectorized, unrolled, functions dropped) on standard error. See Optimization remarks.
-S#
Write the generated code as an assembly file. See Invoking EmbCC.
GCC's optimization options#
A build written for GCC passes GCC's spellings; this is what EmbCC does with each. Migrating from GCC covers the rest of the command line.
| GCC or Clang option | EmbCC |
|---|---|
-O0, -O1, -O2, -Os |
--opt=none, --opt=basic, --opt=speed, --opt=size |
-Og |
--opt=debug: --opt=basic with every source variable kept in its stack slot. |
-O3, -Ofast |
Accepted, the same as --opt=speed. |
-Oz |
--opt=min-size, the same as --opt=size. |
-fNAME, -fno-NAME for a pass NAME |
--pass=NAME, --no-pass=NAME; -finline, -fgcse and the rest name EmbCC's passes. |
-finline-functions, -fno-inline-functions |
--inline=auto, --inline=declared. |
-funroll-loops, -fno-unroll-loops |
--unroll, --no-unroll. |
-ftree-vectorize, -fno-tree-vectorize |
Not accepted. Use --pass=vectorize / --no-pass=vectorize. |
-fwrapv |
--wrap-overflow: accepted; always the behavior. |
-fno-wrapv, -ftrapv |
Not accepted. |
-fstrict-aliasing, -fno-strict-aliasing |
Accepted, no effect; there is no type-based aliasing. |
-fomit-frame-pointer, -fno-omit-frame-pointer |
Accepted, no effect. See Frame pointer. |
-ffunction-sections, -fdata-sections |
--sections=functions, --sections=data: a section per function and per object, for the linker's --gc-sections. See Invoking. |
-ffast-math, -ffp-contract=... |
--fast-math, --fp-contract=...: accepted, no effect. -fno-fast-math is not accepted. |
-flto |
Refused: embcc: error: -flto is not supported; EmbCC would emit ordinary code and the flag's promise would not hold |
__attribute__((optimize(...))), hot, cold, flatten |
Accepted, no effect. The level applies to the whole compilation. |