Ternary operator vs if statement: compiler optimization
Asked Answered
M

6

12

Is this:

int val;  
// ...
val = (val != 0) ? otherVal : 0;

less efficient than this:

int val;
//...
if (val != 0)
    val = otherVal;

?

Are compiler able to optimize the ternary operator? The intent is clear, is there any way it could be wanted to actually write 0 to memory? Maybe when memory is mapped to a file?

Can we assume it doesn't matter?

EDIT: The point is to set a variable to some value if one condition is met. There is no wanted else branching. which is why I ask if a ternary (with obligatory else branch that is supposed to make a copy) will be less efficient or optimized.

Mulberry answered 22/5, 2013 at 16:6 Comment(8)
Duplicit question: https://mcmap.net/q/74282/-ternary-operator-vs-if-elseFranek
I'm not a compiler programmer, so I don't really know, but it could be possible that it evaluates both of the sides of the ternary operator and so skips branching (which would make it fast).Bun
If anything, val is set to 0 when it already is zero, which is unnecessary in the first case. It's very unlikely that the compiler will do anything different tho'. Write the most readable variant.Lathrop
@Bun No, the ?: conditional operator is not allowed to evaluate both of its arguments.Capsular
@Capsular Ok, thanks for the clarification. I remember that it did evaluate both sides in GLSL but had no idea of what it does on the CPU and C++.Bun
It is not a duplicate since I don't ask about if..else but about if alone. The point is: without else, is the ternary operator less efficient. In a ternary you always copy a value to memory but with one if you only copy it in one case. I just wondered if this mattered. Please remove the duplicate tag.Mulberry
@Mulberry there is always an implied else after an if statement, whether you write it or not...Monogamy
@Monogamy If it's just a jmp or a not it doesn't matter as no data is written to memory in the implied else.Mulberry
B
6

Mats Petersson suggestion is generally the best "Write the most readable variant". However, if you are trying to write optimal speed performance code, you need to know more info about your computer and processor. With some machines, the first will run faster (highly pipelined processors: no branching, optimized ternary operator). Other machines will run quicker with the second form (simpler).

Basting answered 22/5, 2013 at 16:30 Comment(7)
The if statement is guaranteed to be at least as fast with even a mediocre optimizing compiler, because it doesn't zero initialize. If it did the same thing, then there would be no performance difference.Perse
@Perse The "guaranteed to be at least as fast" is certainly not in the C spec. The hardware architecture and software techniques of today may easily advance tomorrow and provide a different paradigm. Scalar processors and simply embedded processors offer a wide range of possibilities.Basting
A modern compiler sees both primitives as the same thing, and there is no use case for distinguishing between them. Your answer presents it as a hardware issue but the generated code will always be the same in practice. This isn't going to change as architectures evolve. The compiler will do the same reasoning to decide how to perform the branch regardless of the code style - which is all this is. Sure, a compiler could decide to do optimizations based on whether you use spaces or tabs to indent, but that's not useful as a practical answer.Perse
I'm not making any claims about the C specification. I simply pointed out that an optimizing compiler of even mediocre quality guarantees that these will be treated the same way. If it doesn't treat them the same way, it has a performance bug.Perse
@Perse Interesting assertion: code compiles the same way, if not, the compiler is wrong, not the assertion. Consider an ancient (or novel new platform) using sign-magnitude integers: OP's 2 codes could have different functionality given there are + and - zeros. The first may set val to one of the zeros, the 2nd allows both zeros to persist. The point is not to champion a return to non-2's complement, but to point out C compliant architecture differences can impact supposed equal compilation guarantees.Basting
You're making the demonstrably false claim that this varies in performance between architectures. It's clearly not true because in practice no compiler will generate different code. There is simply no rationale for making optimization decisions based on code style. I am not making any claims about implementation, you are. I'm not claiming that a compiler can't handle each one in a different way, just that in practice they do not, contrary to your claim.Perse
As I pointed out, a compiler could also make different optimization choices based on indent style. They do not need to know more about the platform to make the best choice because any platform with even a mediocre optimizing compiler will see both as the same operation. Generated code does not depend on the code style in practice. Totally equivalent operations like this trivially end up with equivalent compiler IR before it ever gets to the compiler's backend for that architecture at all.Perse
C
6

You could use a branchless ternary operator, sometimes called bitselect ( condition ? true : false).

Don't worry about the extra operations, they are nothing compared to the if statement branching.

bitselect implementation:

inline static int bitselect(int condition, int truereturnvalue, int falsereturnvalue)
{
    return (truereturnvalue & -condition) | (falsereturnvalue & ~(-condition)); //a when TRUE and b when FALSE
}

inline static float bitselect(int condition, float truereturnvalue, float falsereturnvalue)
{
    //Reinterpret floats. Would work because it's just a bit select, no matter the actual value
    int& at = reinterpret_cast<int&>(truereturnvalue);
    int& af = reinterpret_cast<int&>(falsereturnvalue);
    int res = (at & -condition) | (af & ~(-condition)); //a when TRUE and b when FALSE
    return  reinterpret_cast<float&>(res);
}
Chorizo answered 6/5, 2015 at 18:6 Comment(4)
Where did you learn that the ternary operator is branchless?Retinol
Ternary operators are not branchless. There are implementations like bitselect that implement ternary operations for integer in a branchless way (like i've shown here). As you can see, there's no actual ternary operator in the code.Chorizo
Ah, now I see that you implemented it yourself! Nice.Retinol
Modern optimizing compilers will generate branchless code for simple ternarys. Often times more efficient than any bit twiddling non-branching implementation since the semantics aren't hidden from the compiler. You have to check your compiler to see what it does.Herwig
L
4

Your compiler will optimize it. In the end, there is little to no difference in performance.

There is, however, a big difference in readability. Sometimes the ternary operator can help to remove many lines of code that don't add very much in clarity.

In other cases the if statement is clearer and easier to follow.

Reducing code to a ternary statement but then having to add a ton of comments in order to maintain clarity is counterproductive.

And by all the gods of coding, please don't nest ternary statements.

Lintel answered 22/5, 2013 at 16:26 Comment(3)
In C++ you could imagine a copy assignment taking place. For example: "Object a = Object(default); a = (condition) ? Object(var) : Object(default)". Will the compiler be smart enough and aggressive enough to remove the assignment? Sounds dangerous since stuff can happen in constructors. Would it be different for plain old data?Mulberry
"don't nest ternary statements"... unless you have no choice (constexpr comes to mind).Monogamy
there is an optimisation called "copy elision", which allows the compiler prevent some unnecessary copy along the way. I think Object(...) will neve be built since it's a nameless temp and assigned to a variable. en.cppreference.com/w/cpp/language/copy_elision cpp-next.com/archive/2009/08/want-speed-pass-by-valueEvensong
B
2

This is mostly a duplicate of Ternary operator ?: vs if...else

For most compilers the efficiency will be the same and the compiler will optimize the ternary operator just like it optimizes the if/else statement. That said, I prefer if statements as they make the code much easier to read at a quick glance.

To answer your other questions. I'm not sure what you mean, if you are just setting one integer or variable to 0, then there is no faster way other than setting it to zero like you have above.

if you had an array of variables, you could use memset(ptr, 0, size*sizeof(TYPE)), which would probably be fastest if you had an array of variables you wanted to set to zero. Or perhaps std::fill_n

I'm not sure what you're trying to achieve with the logic above, but it seems a little strange. There are ways to arrange code that would mean you probably wouldn't need a conditional in there at all, but it's hard to say for your situation without seeing a bit more of the code.

In all honesty, unless you're doing this operation billions of times, this is probably very pre-mature optimisation and you should concentrate on readability.

Bourges answered 22/5, 2013 at 16:27 Comment(4)
Just trying to understand where readability affects performance. Using a ternary might improve readability in a few occasions but can also induce a performance cost. Meaning that the if-assign would be logically faster but if I wanted to use a ternary to improve readability I would go against that simple logic. If I do that, can the compiler optimize it? It's pretty obvious the ternary-else branch is useless for PoD. But again, when files are mapped to memory or when programming drivers, each write in memory can have an impact... I need to understand how most compiler work in this respect.Mulberry
Most Compilers will optimize ternarys and if/else blocks to exactly the same machine code. Also a ternary almost never makes something more readable, in my humble opinion.Bourges
I think you are right for the readability, however it's subjective and I've seen it used for such justification. Understand that I don't wanna compare ternary to if-else, just to a single if. Meaning the else condition is never taken with the if, whereas it is taken with the ternary... or is it ? (it's my question). It's hard to figure out if the compiler wants to be logical and pedantic or aggressive and optimized. I'm just curious.Mulberry
Before if constexpr came along, the ternary operator was often the only way to declare some compile-time constants. One of my first exercises in recursive templates was a way to calculate the gcd at compile time, and it required lots of ternary operators.Crosseyed
A
2

Tools like "compiler explorer" are great at answering questions like this. Fixing the bug in your code and comparing the following two snippets we see they produce identical assembly at -O1 and higher.

void trinary(int& val, int otherVal) {
    val = (val != 0) ? otherVal : 0;
}

void nontrinary(int& val, int otherVal) {
    if(val != 0) {
        val = otherVal;
    }
    else {
        val = 0;
    }
}

trinary(int&, int):
        mov     eax, DWORD PTR [rdi]
        test    eax, eax
        mov     eax, 0
        cmove   esi, eax
        mov     DWORD PTR [rdi], esi
        ret
nontrinary(int&, int):
        mov     eax, DWORD PTR [rdi]
        test    eax, eax
        mov     eax, 0
        cmove   esi, eax
        mov     DWORD PTR [rdi], esi
        ret

What's interesting is that at -O0 they do not produce identical output. At -O0 the compiler uses eax to explicitly store the result of the trinary operator, and then copies eax into the correct register before returning. The non-trinary version does the assignment directly.

trinary(int&, int):
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     rax, QWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rax]
        test    eax, eax
        je      .L2
        mov     eax, DWORD PTR [rbp-12]
        jmp     .L3
.L2:
        mov     eax, 0
.L3:
        mov     rdx, QWORD PTR [rbp-8]
        mov     DWORD PTR [rdx], eax
        nop
        pop     rbp
        ret
nontrinary(int&, int):
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     rax, QWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rax]
        test    eax, eax
        je      .L5
        mov     rax, QWORD PTR [rbp-8]
        mov     edx, DWORD PTR [rbp-12]
        mov     DWORD PTR [rax], edx
        jmp     .L7
.L5:
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], 0
.L7:
        nop
        pop     rbp
        ret
Amedeo answered 31/10, 2019 at 17:46 Comment(0)
D
0

One big upside of ternary is that you can declare the resuling variable to be const, helping with const correctness at function scope:

const auto value = (condition) ? "true" : "false";`

as opposed to:

auto value = "";

if (condition) {
  value = "true";
} else {
  value = "false"
}

As many others have stated, this isnt likely to cause the compiler to generate different code but it helps to avoid bug and unwanted behaviour caused by reassigning to value.

Decry answered 22/2, 2024 at 9:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.