ARMOptimized:SmallEx:A

Copyright 2018, Clinton A Staley, Sam Meier

Concepts

  1. Reason for optimization
  2. Program 5 MaxOf3

Compiler Optimization

The default compiler from C to assembly is inherently stupid, often writing repetitive and time wasting code, as you've doubtless seen in some previous programs.

Optimization

But there is another aspect to this compiler that not only removes these repetitive tasks, but also outputs efficient code. Compilers will first write the assembly based off the source code, including all of the stupid and redundant things that you've seen before. The compiler will then make a run over the code it just wrote, using various algorithms to determine what can be changed, added or removed to optimize said code.

Currently, all of our previously compiled code used no specific flag for compilation. This tells the computer to throw whatever code it wants into the finished product, so long as the functionality remains what the source code specifies. -O0 is the default 'optimization', that is to say none, while the policy -O3 is the maximum setting, with the highest optimization. There are also some other policies in between and a few edge case policies, but those are seldom used.

So seems great, right? Lets just always use -O3 and get the most optimized, fastest code all the time. Well, not quite.

Question 1

What might we be trading for using the -O3 option for all of our code compilations?

Answer 1

The biggest, and most notable trade off for using more intense compilation optimizations is time. The more algorithms used during the optimization pass, the longer it will take the code to compile.

For older systems, the varying levels of optimization were necessary because of slow hardware. Nowadays, due to the speed of computers, -O3 is the only optimization needed. Programmers of yesteryear would use the either low or no optimization when editing and debugging their code for time purposes.

Program 5

This program, as you probably remember, takes an input of three numbers and finds the highest value of them using two functions called max and maxOf3.

Our prior overview of Prg5.s shows it to be long and very stupid in many places. Prg5Opt.s, compiled with the following command gcc -O3 Prg5.c -o Prg5opt.s solves all of these 'compiler is stupid' moments throughout the code. Let's start by looking at the maxof3 function.

Maxof3

A quick once over of this remarkably short code turns up some interesting omissions. First, there is no mention of either the Frame Pointer or the stack pointer. The optimized version of maxOf3 does not need local variables, nor does it have to store any of it's three formal parameters on the stack. With no information existing on the stack, this eliminates the need to retrieve any information with fp or sp.

Question 2

Why does maxOf3 not need to retrieve its parameters from the stack?

Answer 2

The parameters in question are small enough to be stored in the registers themselves and have no need to be stored in the larger stack memory. The compiler recognized this, and made appropriate changes. Normally, for large numbers, strings or other data types, the parameters location in memory must be pushed onto the stack, simply because the data itself is far to large to be stored in a register.

But there is another glaring problem with this code that at first does not seem to make sense. There is no max function, it simply does not exist in this code, but that does not mean that its functionality is not present. It does exist, just in a different way that what we think. The compiler used a technique with the max function here called inlining. Inlining a function means that the compiler will take the body of the function and splice it where it is called in another. The compiler will make small modifications, such as names of registers, to make sure the code will run without the need to make a function call.

Question 3

Why would the compiler take such measures to avoid a function call?

Answer 3

A function call, as we can see at the assembly level, is a costly thing to do. In many cases, a well optimized compiler will avoid this at any place it can.

The code for max is in-lined twice in maxOf3, once on lines 13 and 14 and again on lines 15 and 16. The C compiler optimizes based on speed, removing redundant commands to create concise and speedy code. In the case of the unoptimized maxOf3 function, the compiler removed many of the mov commands, ones that it saw as unnecessary. The optimized maxOf3, in its best case scenario has no mov commands, with a worst case of two. The non optimized function will always do ten, regardless of the order or value of the numbers given. This optimization allows for much a greater speed of execution in any situation.

let's understand the logic behind how the compiler removes so many of the mov commands from the maxOf3 function.

First, max is only a comparison of two numbers, moving the greater number to the return value of the function. In the case of the first in-lined max function, it has two inputs and a single output. r1 and r2 are the inputs, with r2 also pulling double duty as the output of the function. It will then compare the two numbers, and move r1 into r2 if the number is greater than or equal to. this results in only one mov command executed, and only under a certain condition.

Then the next in-lined max function uses the same logic as the first, but with r1 and r0 being input and r0 being the in-lined function's output. Here is where the compiler did something clever with the return register of the in-line max function and the return register for maxOf3.

Question 4

What did the compiler do that was so clever with the return registers?

Answer 4

The compiler made sure that both the return register from the second in-lined max function lined up with the return function for the overall code of maxOf3, thereby removing the need for another mov command.

In the next lecture, we will look at main.