Clear questions and runnable code
get the best and fastest answer
Trinary Operator Semanticsby hardburn (Abbot)
|on May 26, 2005 at 14:27 UTC||Need Help??|
NOTE: This was orginally a mailing to my local LUG after a Perl vs Python debate and a discussion by one Pythoner about how he's annoyed by the trinary operator.
At a recent LUG meeting, a small debate arose over the trinary operator (so-called because it's the only three-arg operator in most (all?) languages). It was stated that a compiler can create exactly the same code for both the trinary operator and an if-else construct.
It is true that they are functionally identical. In other words, if you create two functions with each construct and give them the same input, they will always return the same output. However, this statement says nothing about the efficiency of the given function.
In programming langauges, there is another kind of identity to keep in mind (and this is the one that I think Python's designers forget a lot): semantic identity. This deals with how the compiler generates the underlieing machine instructions. Optimiziers might seem like they are deep voodoo, but they are actually straightforward mathmatically once you realize how semantics work.
To prove it, I wrote this C program:
This was compiled with gcc -fverbose-asm -S trinary_test.c. This will place the generated assembly code in 'trinary_test.s'. The '-fverbose-asm' arg has gcc make the ASM a little more readable.
My gcc is:
Below are the outputs for the two functions in question. I've put in comments for where the param-passing sections begin and end, which are the same for both functions, since they have the same function signature.
I warn against trying to make an opcode count. Both Intel and AMD stopped publishing opcode counts years ago, because modern processors have so many optimizations built in that you can no longer get a predictable count. Unlike compiler optimizations, I think processor optimizations actually are deep voodoo, to the point where they are boarderline non-deterministic.
What can be compared is that the ifelse has 3 jumps to trinary's 1. That will be a significant hit on performance, especially if the processor makes a branch mis-prediction.
Also, notice how the two handle the return variable:
The trinary function is handling the variable between registers, whereas ifelse is using a memory location. This is another boost to the trinary operator.
Semantics are basically a way to give the compiler more information about what you're trying to do. In this case, the trinary operator is designed around the idea that you will directly assign to a variable (to_return = var ? 1 : 0;). OTOH, an if-else may or may not be used that way, so it must be compiled in a much more general way.
As it happens, putting -O2 in the compiler args has both functions compile to identical assembly. Apparently, gcc is smart enough to figure out that the if-else's only side effect is modifying the to_return variable. Consider all this a lesson in how semantics work in general, because such smart optimization may not always be possible without introducing different syntax to produce better semantics. Also, it may not be possible for gcc to do so when the operation is placed inside a much more complex function.
All this may not apply to other programming languages. I suspect Perl's implementation isn't very efficient, on the basis that Perl's semantics in general make C look good (it only got constant-folding fairly recently). It isn't so much that Perl's designers ignored good semantics, but that the language wasn't really designed at all. Perl6 should be a great improvement here.
Since the orginal writing, I've been shown output from B::Terse showing that Perl will indeed do optimization of the trinary operator.
Language designers often make a less-general alternative to another statement that has better semantics (particularly the (semi-)functional languages). That's why C's switch() statement is limited in the conditionals it can handle compared to a if-elseif-else. The limitation means switch() can be compiled to a jump table with O(1) efficiency, whereas the if-elseif-else is O(n). (See Re: Perl Idioms Explained: && and || "Short Circuit" operators for an interesting discussion on this).
Functional languages won't generally force a programmer to learn syntax for the sake of learning syntax (i.e., obfuscation), but they freely add (or remove) syntax if it means the compiler can reason about the program more effectively. While this admittedly makes the learning curve harder, it also produces a much more efficient language.
"There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.