Lost in translation (C vs. Assembly Language)

In my classes on microcontroller programming I often draw comparisons between C and assembly language. This is to show my students that either programming language has both advantages and disadvantages. While the efficiency of C code predominantly depends on how ‘clever’ the compiler is, the efficiency of assembly programs almost entirely depends on the cleverness of the human programmer. The reason is that the content of the machine code instruction has been determined by the hardware manufacturer. This offers a larger number of possible solutions for one specific problem. A C compiler, however, will translate your source code into machine code instructions that ‘seem’ reasonable and efficient in one specific situation.

Here are two examples I use in my classes. They should work for any 8051 compatible CPUs.

Example #1

if( a == 10 ) {
   // do something
}
else {
   // do something else
}

could be translated to the following assembly code.

           cjne a, #10, not_equal
           ; do something
           jmp cont
not_equal: ; do something else
cont:      ; continue program ...

The cjne instruction (Carry Jump Not Equal) compares register a (accumulator) and the decimal constant of 10. If a is not equal to 10, the CPU will jump to the label not_equal. If a is equal to 10, the next command will be executed. In this example the CPU will continue the program after the control structure. You will probably agree that the principle above is pretty much straight forward and easy to understand.

In assembly language there is a different approach to accomplish exactly the same task, though. Consider the following code.

          xrl a, #10
          jz is_equal
          ; do something else
          jmp cont
is_equal: ; do something
cont:     ; continue program ...

In order to fully understand this code, you need to have some basic knowledge on logic functions. The xrl instruction performs an XOR operation between the accumulator and the constant value of 10. An XOR function actually measures the ‘grade of differentness’ between two values. If the values are equal, the result is zero. (If you neither know nor believe this, just have a try with pencil and paper.)

So all we have to after the XOR operation is to check whether the result (that is to be found in the accumulator) is 0. If it is zero, the CPU will jump to the label named is_equal. If the result is any value other than 0, the program continues right after the JZ (jump if accumulator zero) instruction.

It is difficult to say which of the two translations is better. Which one you should prefer will depend on a variety of criteria, e.g. runtime or storage optimization, the context (how the code fits into your program), etc.

Example #2

if( a < 100 ) {
   // do something
}
else {
   // do something else
}

Again, the cjne instruction could provide the basis of a straight forward solution. cjne compares two values, but it also sets or clears the CPUs carry bit to express ‘less than’ or ‘greater than’ respectively. So we can first use cjne on a and the constant value of 100, and then check the state of the carry bit. If the carry bit is 1, a is less than 100. If the carry bit is 0, a is greater than 100. The according assembly code would go like this:

           cjne a, #100, not_equal
           ; do something else (here a==100)
           jmp cont
not_equal: jc less_than
           ; do something else (here a>100)
           jmp cont
less_than: ; do something (here a<100)
cont:      ; continue program ...

This solution includes a lot of jmp commands, which – as you might expect – will slow down your program significantly. In fact, there is a much smarter solution in assembly code.

           subb a, #100
           jc less_than
           ; do something else (a>=100 here)
           jmp cont
less_than: ; do something (a<100 here)
cont:      ; continue program ...

This smarter way relies on the fact that if you subtract any number from a smaller number you will cause an underflow, which in turn causes the CPU to set the carry bit.

There are a lot more examples that show the influence of more or less ‘intelligent’ compilers … or programmers.

In higher programming languages, such as Java, that are being employed in an environment of sophisticated operating systems and/or virtual machines, you will often see no difference in code efficiency. This is for two reasons:
1. The underlying software layers will weed out most of your programming sins
2. In systems of high (and cheap) hardware performance it often is legitimate ‘wasting’ a couple of nanoseconds by running less efficient code. After all, a lot of hardware performance is being spent on reducing the semantic gap between human and machine, anyway.

See you in class,

— Andre M. Maier

Advertisements

About bitjunkie

Teacher, Lecturer, and BITJUNKIE ...
This entry was posted in Programming Essentials, Uncategorized and tagged , , , . Bookmark the permalink.