Good programming practice is to check the validity of parameters passed into a function and to return an appropriate error. Over the many years, I've seen different ways of writing these checks. Here, I present four common styles and how it affects performance.
Below is a pretty common style. Check for errors early and return any error codes. The advantage is that the operational code is not indented or made more complex by the input checks. The disadvantage is that you need to place multiple breakpoints to check the return value when debugging.
(While I could have combined the first two if statements for a fair comparison to the styles that follow, this provides three branches taken during normal operation, which showed interesting results.)
Some coding standards insist on only one exit point. For that style, use a local variable and else statements.
The only issue with the first two styles is that the CPU needs to skip over bytes when everything is valid. That is, if the inputs are all good, the CPU needs to skip over the error code to get to the operation portion. Below, we flip the comparisons so that the operation code is closer in memory. The downside is that the error assignment code is far away from the associated comparison statement, which is more difficult for a human to check for correctness and to maintain.
More recently, I picked up this style. Set the error code first, but compare in such a way that valid inputs don't branch. This keeps the error codes besides the comparison and has the fewest branches. The operational code does get indented, though. And, we make excess assignments.
CODE SIZE
If code size is critical, the ValidFirst style uses the fewest bytes (in this case 24 vs 30), because of the lack of else statements. You see, each else statement requires a hidden branch for the previous code to jump over it. I used this style in TinyTransfer which had to fit on a Macintosh 128K.
CODE SPEED
Now for the fun part: how much of a performance difference does this make? Let's assume this is a routine which is called a massive number of times in a loop. It turns out that the cache, bus size, and pipeline capabilities of the 68K CPU models cause significantly different results.
The 68000 CPU is pretty boring. The fewest branch instructions and fewest branches taken provides a mild speed improvement of 104% of the average speed of all four styles on the Macintosh SE.
The 68020 CPU added an instruction pipeline. But, every time a branch is taken, the pipeline is cleared. Worse still, the LC has a 16-bit bus, making refilling the pipeline more expensive (but why isn't the L1 cache compensating?). So, the straight-through (no branch) path of the ValidFirst style more than makes up for the extra error code assignment statements. The 32-bit bus on the Macintosh II helps, but not branching is still best.
The 68030 CPU is similar to the 68020 from a pipeline perspective. So, we are still seeing how the no-branch path is most efficient. Yet, when an L2 cache is added in a IIci, the branch penalty is reduced. I assume the pipeline is refilled quickly from the cache.
The 68040 CPU switched assumptions. It assumes branches will be taken. Thus, the Multiple Return style (which each if statement is assumed branched over by the 68040) gets us to the operation fastest. But the 68040 hedged bets by filling both taken and not-taken pipelines. Thus, the results are remarkably close to the cached IIci, only flipped.
The Basilisk II emulating a 68040 tightened the differences further.
CONCLUSION
If you're writing a tight loop with if statements for the pre-68040 era Macs, try to make it flow though without taking any branches. On faster Macs, the speed penalty is negligible, particularly given the machine is already so much faster.
For code where performance is not critical, write in a style that is easiest to maintain and read.
- David
Below is a pretty common style. Check for errors early and return any error codes. The advantage is that the operational code is not indented or made more complex by the input checks. The disadvantage is that you need to place multiple breakpoints to check the return value when debugging.
(While I could have combined the first two if statements for a fair comparison to the styles that follow, this provides three branches taken during normal operation, which showed interesting results.)
Some coding standards insist on only one exit point. For that style, use a local variable and else statements.
The only issue with the first two styles is that the CPU needs to skip over bytes when everything is valid. That is, if the inputs are all good, the CPU needs to skip over the error code to get to the operation portion. Below, we flip the comparisons so that the operation code is closer in memory. The downside is that the error assignment code is far away from the associated comparison statement, which is more difficult for a human to check for correctness and to maintain.
More recently, I picked up this style. Set the error code first, but compare in such a way that valid inputs don't branch. This keeps the error codes besides the comparison and has the fewest branches. The operational code does get indented, though. And, we make excess assignments.
CODE SIZE
If code size is critical, the ValidFirst style uses the fewest bytes (in this case 24 vs 30), because of the lack of else statements. You see, each else statement requires a hidden branch for the previous code to jump over it. I used this style in TinyTransfer which had to fit on a Macintosh 128K.
CODE SPEED
Now for the fun part: how much of a performance difference does this make? Let's assume this is a routine which is called a massive number of times in a loop. It turns out that the cache, bus size, and pipeline capabilities of the 68K CPU models cause significantly different results.
The 68000 CPU is pretty boring. The fewest branch instructions and fewest branches taken provides a mild speed improvement of 104% of the average speed of all four styles on the Macintosh SE.
The 68020 CPU added an instruction pipeline. But, every time a branch is taken, the pipeline is cleared. Worse still, the LC has a 16-bit bus, making refilling the pipeline more expensive (but why isn't the L1 cache compensating?). So, the straight-through (no branch) path of the ValidFirst style more than makes up for the extra error code assignment statements. The 32-bit bus on the Macintosh II helps, but not branching is still best.
The 68030 CPU is similar to the 68020 from a pipeline perspective. So, we are still seeing how the no-branch path is most efficient. Yet, when an L2 cache is added in a IIci, the branch penalty is reduced. I assume the pipeline is refilled quickly from the cache.
The 68040 CPU switched assumptions. It assumes branches will be taken. Thus, the Multiple Return style (which each if statement is assumed branched over by the 68040) gets us to the operation fastest. But the 68040 hedged bets by filling both taken and not-taken pipelines. Thus, the results are remarkably close to the cached IIci, only flipped.
The Basilisk II emulating a 68040 tightened the differences further.
CONCLUSION
If you're writing a tight loop with if statements for the pre-68040 era Macs, try to make it flow though without taking any branches. On faster Macs, the speed penalty is negligible, particularly given the machine is already so much faster.
For code where performance is not critical, write in a style that is easiest to maintain and read.
- David