C Compilers and
Optimization:
Module 3
Shana Santhosh
Basic C Data Types
• ARM processors have 32-bit registers and 32-bit data processing
operations.
• The ARM architecture is a RISC load/store architecture
• In other words you must load values from memory into registers
before acting on them.
• There are no arithmetic or logical instructions that manipulate values
in memory directly
• Early versions of the ARM architecture (ARMv1 to ARMv3) provided
hardware support for loading and storing unsigned 8-bit and unsigned
or signed 32-bit values
• The ARMv4 architecture and above support signed 8 bit and 16 bit
load and stores directly,through new instruction
• ARMv5 adds instruction support for 64 bit load stores
• Prior to ARMv4 ,ARM processors were not good at handling signed 8
bit or any 16 bit [Link] ARM C compiler define char to be
unsigned 8 bit value rather than signed 8 bit value
• Compilers armcc and gcc uses the data types mapping in table for an
ARM target
Local variable type
• ARMv4 based processors can efficiently load and store 8,16 and 32 bit
[Link] most ARM data processing operations are 32 bit
[Link] this reason ,we should use a 32 bit data type int,or long ,for
local variables where ever possible
• Avoid using char and short local variable types even if you are
manipulating an 8 bit or 16 bit value.
• The one exception is when u want a wrap around to [Link] you
require modulo arithmetic of the form 255+1=0,then use a char type
• The following code checksums a data packet containing 64 [Link]
shows why we should avoid using char for local variables
Now Compare this to the compiler output where instead we declare I as an
unsigned int.
In the first case ,the compiler inserts an extra AND instruction to reduce i to
the range 0 to 255 before the comparison with 64 .This instruction disappears
in the second case.
Next, suppose the data packet contains1 6-bitvalues and we need [Link]
is tempting to write the following C code
}
Return sum
The LDRH instruction does not allow for a shifted address offset as the LDR
instruction did in checksum_v2. Therefore the first ADD in the loop calculates the
address of item I in the array. The LDRH loads from an address with no offset.
LDRH has fewer addressing modes than LDR as it was a later addition to the ARM
instruction set.
■ The cast reducing total + array[i] to a short requires two MOV instructions. The
compiler shifts left by 16 and then right by 16 to implement a 16-bit sign extend.
The shift right is a sign-extending shift so it replicates the sign bit to fill the upper
16 bits
Function Argument Types
• Converting local variables from types char or short to type int
increases performance and reduces code size. The same holds for
function arguments
• . Consider the following simple function, which adds two 16-bit
values, halving the second, and returns a 16-bit sum:
• short add_v1(short a, short b)
• {
• return a + (b>>1);
• }
• Narrow vs. Wide Argument Passing
• Narrow Passing:The caller (the code that calls the function) makes
sure the arguments fit into the smaller type (like short or char) before
calling the function.
• The function (callee) assumes the arguments are already the correct
size and doesn't need to do any extra work.
• This can make the function run faster but adds work to the caller
• Wide Passing:
• The caller passes the arguments using the full size of the register (like
a 32-bit integer) without worrying about the smaller type.
• The function then adjusts the arguments to fit the smaller type.
• This shifts the work to the function itself and can make the function
slower but makes the caller's job easier.
• Example
• Consider the function
• short add_v1(short a, short b)
• {
• return a + (b >> 1); }.
• Narrow Passing (used by armcc compiler):The caller converts a and b to 16-
bit values before passing them to add_v1.The function just adds the values
without needing to adjust [Link] approach reduces the function's code
but makes the caller's code larger.
• Wide Passing (used by gcc compiler):The caller passes a and b as full 32-bit
values. The function converts these values to 16-bit values before using
them.
• This makes the function's code larger and potentially slower but keeps the
caller's code smaller.
• For armcc in ADS, function arguments are passed narrow and values
returned narrow
• The gcc compiler we used is more cautious and makes no
assumptions about the range of argument value. This version of the
compiler reduces the input arguments to the range of a short in both
the caller and the callee. It also casts the return value to a short type.
Here is the compiled code for add_v1:
• Why Use int Instead of char or short?
• Using int for function arguments and return values, even for small
data, avoids the need for these extra conversions:
• No need to cast values up or down in size.
• Reduces the overall code size.
• Improves performance since the CPU can handle int types more
efficiently.
• In summary, using int for function arguments and return values is
usually more efficient because it avoids the extra work of converting
between different sizes.
Signed versus Unsigned Types
• The previous sections demonstrate the advantages of using int rather
than a char or short type for local variables and function arguments.
This section compares the efficiencies of signed int and unsigned int.
• If your code uses addition, subtraction, and multiplication, then there
is no performance difference between signed and unsigned
operations.
• However, there is a difference when it comes to division. Consider the
following short example that averages two integers:
• When writing efficient code for ARM architectures, particularly
ARMv4, certain practices can significantly enhance performance
➢Avoid char and short Types:
➢Reason: Local variables held in registers should generally be int or
unsigned int unless 8-bit or 16-bit modular arithmetic is required.
➢Performance: Using int types avoids the need for extra instructions
that convert between smaller types and int, making operations faster
➢Use Unsigned Types for Divisions:
• Reason: Unsigned integer division is faster because the compiler can
replace division by powers of two with logical right shifts, which are
simpler and faster operations.
➢Arrays and Global Variables in Main Memory
• Use Smallest Possible Types:
• Reason: For array entries and global variables in memory, using the smallest
data type that fits the required data reduces the memory footprint.
• Avoid Offsets with Short Type Arrays:
• Reason: The LDRH instruction (Load Register Halfword) does not
support offsets. It's more efficient to increment the array pointer
directly.
• Local Variables: Prefer int or unsigned int over char or short unless
specific smaller-width arithmetic is needed.
• Array and Global Variables: Use the smallest suitable type and
increment pointers for array traversal.
• Casting: Use explicit casts for clarity and efficiency, and avoid
narrowing casts in expressions
• .Function Arguments and Return Values: Use int type to avoid
unnecessary casting overhead.
C LOOPING STRUCTURES
• Loops With A Fixed Number Of Iterations
• Loops With A Variable Number Of Iterations.
• Loop Unrolling
Loops With A Fixed Number Of Iterations
This data packet checksum routine shows how the compiler treat a loop
with incrementing count
• This example shows the improvement if we switch to a
decrementing loop rather than an incrementing loop
• The loop counter should count down to zero rather than counting up
to some arbitrary limit
• Then the comparison with zero is free since the result is stored in the
conditional flag.
• Signed and unsigned loop counter
• For an unsigned loop counter i we can use either of the loop
continuation conditions i!=0 ori>[Link]’t be negative, they are the
same condition
• For a signed loop counter, it is tempting to use the condition i>0 to
continue the loop. You might expect the compiler to generate the
following two instructions to implement the loop:
• Therefore you should use the termination condition i!=0 for signed or
unsigned loop counters. It saves one instruction over the condition
i>0 for signed i.
• Loops Using a Variable Number of Iterations
• Now suppose we want our checksum routine to handle packets of
arbitrary size. We pass in a variable N giving the number of words in
the data packet. Using the lessons from the last section we count
down until N = 0 and don’t require an extra loop counter i.
Notice that the compiler checks that N is nonzero on entry to the function. Often this check is
unnecessary since you know that the array won’t be empty. In this case a do-while loop gives better
performance and code density than a for loop.
• Loop Unrolling
• In decrement loop, each loop iteration costs two instructions in
addition to the body of the loop: a subtract to decrement the loop
count and a conditional branch.
• We call these instructions the loop overhead.
• On ARM7 or ARM9 processors the subtract takes one cycle and the
branch three cycles, giving an overhead of four cycles per looping
• We can save some of these cycles by unrolling a loop—repeating the
loop body several times, and reducing the number of loop iterations
by the same proportion. For example, consider unrolling of packet
checksum example four times.
• The following code unrolls our packet checksum loop by four times. We
assume that the number of words in the packet N is a multiple of four
• There are two questions you need to ask when unrolling a loop:
• ■ How many times should unroll the loop?
• ■ What if the number of loop iterations is not a multiple of the unroll
amount?
• For example, what if N is not a multiple of four in checksum_v9?
• To start with first question ,only unroll loops that are important for
the overall performance of the application ,otherwise unrolling will
increase the code size with little performance benefit
• For the second question, try to arrange it so that array size are
multiples of your unroll amount. if it is not possible then you must
add extra code to take over the left over case. This increases the code
size a little but keeps the performance high.
• int checksum_v10(int *data, unsigned int N)
• {
• unsigned int i;
• int sum=0;
• for (i=N/4; i!=0; i--)
• {
• sum += *(data++);
• sum += *(data++);
• sum += *(data++);
• sum += *(data++);
• }
• for (i=N&3; i!=0; i--)
• {
• sum += *(data++);
• }
• return sum;
• }
Writing Loops Efficiently
■ Use loops that count down to zero. Then the compiler does not need to allocate a register to hold the
termination value, and the comparison with zero is free.
■Use unsigned loop counters by default and the continuation conditioni!=0 rather than i>0. This will
ensure that the loop overhead is only two instructions.
■ Use do-while loops rather than for loops when you know the loop will iterate at least once. This saves
the compiler checking to see if the loop count is zero.
■ Unroll important loops to reduce the loop overhead. Do not ove runroll. If the loop overhead is small as
a proportion of the total, then unrolling will increase code size and hurt the performance of the cache.
■ Try to arrange that the number of element in arrays are multiples of four or eight. You can then unroll
loops easily by two, four, or eight times without worrying about the leftover array elements.
Register Allocation
• The compiler attempts to allocate a processor register to each local
variable you use in a C function.
• It will try to use the same register for different local variables if the use of
the variables do not overlap.
• When there are more local variables than available registers, the compiler
stores the excess variables on the processor stack.
• These variables are called spilled or swapped out variables since they are
written out to memory (in a similar way virtual memory is swapped out to
disk).
• Spilled variables are slow to access compared to variables allocated to
registers
• To implement a function efficiently, you need to
• ■ minimize the number of spilled variables
• ■ ensure that the most important and frequently accessed variables
are stored in register
• If the compiler does need to swap out variables, then it chooses
which variables to swap out based on frequency of use.
• A variable used inside a loop counts multiple times. You can guide the
compiler as to which variables are important by ensuring these
variables are used within the innermost loop
• The register keyword in C hints that a compiler should allocate the
given variable to a register
• Summary
• Efficient Register Allocation
• ■ Try to limit the number of local variables in the internal loop of
functions to 12. The compiler should be able to allocate these to ARM
registers.
• ■ You can guide the compiler as to which variables are important by
ensuring these variables are used within the innermost loop
Function Calls
• The ARM Procedure Call Standard (APCS) defines how to pass
function arguments and return values in ARM registers.
• The more recent ARM-Thumb Procedure Call Standard (ATPCS) covers
ARM and Thumb interworking as well.
• The first four integer arguments are passed in the first four ARM
registers: r0, r1, r2, and r3.
• Subsequent integer arguments are placed on the full descending
stack, ascending in memory
• Two-word arguments such as long long or double are passed in a pair
of consecutive argument registers and returned in r0, r1. The
compiler may pass structures in registers or by reference according to
command line compiler options.
• The first point to note about the procedure call standard is the four-
register rule. Functions with four or fewer arguments are far more
efficient to call than functions with f ive or more arguments.
• For functions with four or fewer arguments, the compiler can pass all
the arguments in registers. For functions with more arguments, both
the caller and callee must access the stack for some arguments
• If your C function needs more than four arguments, or your C++
method more than three explicit arguments, then it is almost always
more efficient to use structures.
• Group related arguments into structures, and pass a structure
pointer rather than multiple arguments
• Cont..
• The next example illustrates the benefits of using a structure pointer.
First we show a typical routine to insert N bytes from array data into a
queue.
• We implement the queue using a cyclic buffer with start address
Q_start(inclusive) and end address Q_end(exclusive).
Compare this with a more structured approach using
three function arguments
• The following code creates a Queue structure and passes this to the
function to reduce the number of function arguments.
• The queue_bytes_v2 is one instruction longer than queue_bytes_v1,
but it is in fact more efficient overall.
• The second version has only three function arguments rather tha five.
• Each call to the function requires only three register setups. This
compares with four register setups, a stack push, and a stack pull for
the first version. There is a net saving of two instructions in function
call overhead.
• There are likely further savings in the callee function, as it only needs
to assign a single register to the Queue structure pointer, rather than
three registers in the nonstructured case.
• Calling Functions Efficiently
• ■ Try to restrict functions to four arguments. This will make them
more efficient to call.
• Use structures to group related arguments and pass structure
pointers instead of multiple arguments.
• ■ Define small functions in the same source file and before the
functions that call them.
• The compiler can then optimize the function call or inline the small
function.
• ■ Critical functions can be in lined using the __inline keyword
Pointer Aliasing
• Two pointers are said to alias when they point to the same address.
• If you write to one pointer, it will affect the value you read from the
other pointer.
• In a function, the compiler often doesn’t know which pointers can
alias and which pointers can’t.
• The compiler must be very pessimistic and assume that any write to
a pointer may affect the value read from any other pointer, which can
significantly reduce code efficiency.
• Let’s start with a very simple example. The following function
increments two timer values by a step amount
• This compiles to
• In this case the second value of *step is different from the first and
has the value *timer1.
• This forces the compiler to insert an extra load instruction
• The fix is easy: Create a new local variable to hold the value of state-
>step so the compiler only performs a single load.
• In the code for timers_v3we use a local variable step to hold the value
of state->step Now the compiler does not need to worry that state
may alias with timers
• Another pitfall is to take the address of a local variable
• . Once you do this, the variable is referenced by a pointer and so
aliasing can occur with other pointers.
• Consider the following example, which reads and then checksums a
data packet
.
To avoid this, don’t take the address of local variables. If you must do this, then copy
the value into another local variable before use.
• AvoidingPointerAliasing
• ■ Do not rely on the compiler to eliminate common subexpressions
involving memory accesses. Instead create new local variables to hold
the expression. This ensures the expression is evaluated only once.
• ■ Avoid taking the address of local variables. The variable may be
inefficient to access from then on.
Portability Issues
• When transitioning C code to ARM architecture, several portability
challenges can arise, particularly concerning data types and their
behaviors
• The char type. On the ARM, char is unsigned rather than signed as for
many other processors. A common problem concerns loops that use a
char loop counter i and the continuation condition i ≥ 0, they become
infinite loops
• In this situation, armcc produces a warning of unsigned comparison
with zero .You should either use a compiler option to make char
signed or change loop counters to type int
• The int type. Some older architectures use a 16-bit int, which may
cause problems when moving to ARM’s 32-bit int type although this is
rare nowadays. Note that expressions are promoted to an int type
before evaluation. Therefore if i =-0x1000,
• the expression i == 0xF000 is true ona16-bit machine butfalse on a
32-bit machine
• Note: The 2’s complement of -0x1000 is 0xF000.
• Endian assumptions.
• C code may make assumptions about the endianness of a memory
system, for example, by casting a char * to an int *.
• If you configure the ARM for the same endianness the code is
expecting, then there is no issue. Otherwise, you must remove
endian-dependent code sequences and replace them by endian-
independent one
• Function prototyping.
• The armcc compiler passes arguments narrow, that is, reduced to the
range of the argument type. If functions are not prototyped correctly,
then the function may return the wrong answer.
• Other compilers that pass arguments wide may give the correct
answer even if the function prototype is incorrect. Always use ANSI
prototypes.
• Use of bit-fields. The layout of bits within a bit-field is implementation
and endian dependent. If C code assumes that bits are laid out in a
certain order, then the code is not portable.
• Useof enumerations. Although enum is portable, different compilers
allocate different numbers of bytes to an enum. The gcc compiler will
always allocate four bytes to an enum type.
• The armcc compiler will only allocate one byte if the enum takes only
eight-bit values. Therefore you can’t cross-link code and libraries
between different compilers if you use enums in an API structure
• Inline assembly. Using inline assembly in C code reduces portability
between architectures.
• You should separate any inline assembly into small inlined functions
that can easily be replaced.
• It is also useful to supply reference, plain C implementations of these
functions that can be used on other architectures, where this is
possible
• The volatile keyword. Use the volatile keyword on the type definitions
of ARM memory-mapped peripheral locations. This keyword prevents
the compiler from optimizing away the memory [Link] also ensures
that the compiler generates a data access of the correct type.
• For example, if you define a memory location as a volatile short
• type, then the compiler will access it using 16-bit load and store
instructions LDRSH and STRH.