Memory is a costly commodity in the world of embedded software development. Software programmers often need to optimize the code to reduce memory utilization or to increase the speed of execution. It is the last step in software development, but this is not always in the case of embedded system development. To save the cost of the product, hardware designers design the system having barely enough memory and processing power to get the job done. The goal of code optimization is to make the working program run on the lower cost production version of the hardware.
If you are struggling with low memory size and slow processing speed, these are some code optimization techniques you can implement into your project to increase the code efficiency and to save some amount of memory.
Increasing Code Efficiency:
Modern compilers provide some degree of code optimization. However, most of the optimization techniques of the compiler involve a trade-off between execution speed and code size. An improvement in one area can have a negative impact on another. Your program can be either faster or smaller, but not both. It is a programmer’s choice to decide which of these improvements is most important.
As execution speed is usually important only in certain time critical or frequently executed sections of the code, it is recommended to reduce the size of the program, and improve the efficiency of those time critical or frequently executed sections by hand. However, code size is a difficult thing to influence manually, and compiler optimization is better option to make this change across all software modules.
After compiler’s optimization process, you can identify the sections/routines that require greater code efficiency. You can use the following techniques to reduce the execution time of such sections/routines.
Inline functions
“inline” keyword makes a request to the compiler to replace all calls to the indicated function with copies of the code that is inside. This eliminates the run-time overhead associated with the actual function call. This is most effective when the inline function is called frequently but contains only a few lines of code. Though inline function improves execution speed, it increases the code size.
Table lookups
A “switch” statement is one of the common decision-making technique used in programming. It needs to be used with care because each test and jump in switch statement uses valuable processor time simply deciding what work should be done next. By putting the most likely cases first and the least likely cases last we can reduce the average execution time.
If there is a lot of work to be done within each case, it might be more efficient to replace the entire switch statement with a table of pointers to functions. For example,
int completeAction1 (void);
int completeAction2 (void);
int completeAction3 (void);
int ActionIndex;
ActionIndex = getActionIndex();
switch (ActionIndex)
{
case ACTION1:
completeAction1 ();
break;
case ACTION2:
completeAction2 ();
break;
case ACTION3:
completeAction3 ();
break;
}
This code can be optimized by using function pointers as below.
void completeAction1 (void);
void completeAction2 (void);
void completeAction3 (void); /* define function pointer array */
void (*ActionFunctions[ ]) () = {completeAction1, completeAction2, completeAction3}; /* The switch statement can be replaced by*/
int ActionIndex;
ActionIndex = getActionIndex ();
ActionFunctions (ActionIndex);
Hand coded assembly
Hand coded assembly gives the programmer an opportunity to make software modules as efficient as possible. A good programmer can write better machine code than an average compiler for a given function.
Register variables
For frequently accessed variables, you can use the keyword “register”. This tells the compiler to store the variable into a general purpose register, rather than on the stack. This enhances the performance of the function.
Global variables
Global variables eliminate the need to push the parameter onto the stack before the function call and pop it back off once the function is completed. Therefore, it is more efficient to use a global variable than to pass a parameter to a function.
Though the global variables save some ROM size, they take up a permanent place in RAM, so they increase RAM consumption. Also, global variables are visible to all modules and can be easily modified by any other module so the global variables must be used with more care.
Decreasing Code Size:
When it comes to reducing code size, your compiler’s optimization is the best option. However, if the resulting program is still too large for your available ROM, there are many techniques you can use to further reduce the size of your program.
Natural word size
ANSI C and C++ standards state that data type int must always map to processor’s native word size. It requires the use of additional machine language instructions to perform manipulation of smaller and larger data types. So it’s a good practice to use int whenever possible in your program.
Goto statement
Using goto statement, you can remove the complicated control structures or share a block of repeated code. goto statement generally disrupts code structure, flow, and readability. If used inappropriately, goto statement can even break your code. So, my advice is to use goto when it is absolutely necessary.
Avoid standard library routines
Large standard library routines are expensive only because they try to handle all possible cases. It might be possible to implement a subset of the functionality yourself with significantly less code. This is why it is good to avoid standard library routines.
Techniques described in the previous section specifically table lookups, hand-coded assembly, register variables, and global variables are also helpful in reducing code size. Of these, the use of hand-coded assembly yields the largest reduction in code size.
Reducing Memory Usage:
In some applications, RAM is the limiting factor rather than ROM. In such cases, you need to reduce your dependence on global data, the stack, and the heap. A programmer can make these optimizations better than a compiler.
“const” keyword
Using const keyword before your constant data, you can instruct the compiler to store that data into the ROM. Most compilers place all constant global data into a special data segment that is recognizable to the locator as ROM-able. This is most valuable when you have lots of strings or table-oriented data that does not change at run-time.
Stack size reduction
RAM requirement of your program can be lowered by reducing stack size. In case of real-time operating systems, you need to be conscious of stack space. Most of the operating systems create a separate stack for each task. You can determine the amount of stack required for each task. You might also try to reduce the number of tasks that have a separate “interrupt stack” for the execution of all interrupt service routines.
Using one or more of these optimization techniques you can improve code quality and efficiency of your program.
Note: Never make the mistake of assuming that the optimized program will behave the same as the unoptimized one. You must completely retest your program at each new optimization level to be sure its behavior has not changed.