(C) 2009 Hank Wallace
This series of articles concerns embedded systems design and programming, and how to do it with excellence, whether you are new to the discipline or a veteran. This article is about understanding your compiler.
The compiler’s job is to accurately and efficiently map source statements to CPU instructions. This is a very complex task and much investigation has been done in the last 50 years in this area. But since it is such a complex task, there are lots of opportunities for error and misunderstanding, so if you don’t understand how the compiler works (at a high level), you are in for some trouble. Too many programmers simply trust the code that the compiler produces. Our attitude should rather be one of justified suspicion.
If you are like me, you have several compilers installed on your development machines. Mine are C or C++ compilers, but they are all different. Even successive versions of the same product from the same vendor may break your existing applications. That means that we must find some commonality between all these compilers in order to write programs that run properly.
A huge issue is the variability of data sizes. It bothers me that Kernigan and Ritchie did not name their data types by the number of bits contained in each. That would have eliminated billions of lines of preprocessor code (likely preventing global warming). As a fix, every programmer on the planet defines his own types containing the numbers 8, 16, 32, and 64.
Programming for portability is good, to the extent possible, but it’s also required that our systems must perform efficiently. That means we have to know and understand the most efficient data type that each processor can handle. For low end PICs, that’s a byte. For MSP430 and similar micros, that’s a 16-bit word. For X86 CPUs, that’s a 32-bit word. The ARM processors can run in multiple modes, so the most efficient data type may vary.
Now for the compiler purists, changing your programming techniques based on the platform’s characteristics is a capital heresy. Unfortunately, we don’t work in an academic utopia, but rather in a world that rewards economic success and punishes failure. Do not be intimidated by the purists. Do the right thing for your product and company.
To get an idea of how your compiler works, compile some sample routines using various data types. You will find that operations on 16-bit and longer types on PIC processors generate a HUGE amount of code. I was doing some audio DSP work with an MSP430 and examined the compiler output to see how I could optimize the 16-bit math. It turned out that each C statement evaluated to one CPU instruction. That’s about as efficient as it gets.
In many cases, you can break up complex mathematical and logical expressions and improve efficiency. In fact, most compiler bugs I have encountered were exposed by complex math expressions. I worked around the problems by splitting up single expressions into a sequence of simpler expressions.
Many programmers rely on the optimizer to save space and speed up their code to the point where it will actually work. Optimizers are OK, but your code should be able to run without such processing, in case you have to debug the program in detail. Also, optimizers can break your code in obscure and difficult to diagnose ways. Every compiler I have used has had optimizer problems. Generally the lesser levels of size and speed optimization are safe, and they get you 80% of the benefit anyway.
And please understand that in spite of published standards, not all compilers parse all expressions with the same precedence of operators. The cure? Parenthesize EVERYTHING.
Most compilers these days have an array of nonstandard features that can make your life easier. That’s great, but it makes it tough to run a program on more than one CPU. Sometimes I will test an algorithm on a PC, then compile the code into an embedded system. Nonstandard compiler features make this difficult. Always use the smallest possible subset of the compiler’s proprietary feature set.
When you do find a compiler bug, document it prominently in the source and don’t repeat that mistake. It helps to create small test case that demonstrates the bug. Send it to the vendor, but don’t hold your breath. I’ve done this many times but have never received a bug fix. It might help someone in the next decade, however.
Hank Wallace is the owner of Atlantic Quality Design, Inc., a consulting firm located in Fincastle, Virginia. He has experience in many areas of embedded software and hardware development, and system design. See www.aqdi.com for more information.