11

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.

user207421
  • 305,947
  • 44
  • 307
  • 483
Aki
  • 329
  • 3
  • 13
  • 28
  • 3
    Duplicit question: http://stackoverflow.com/questions/3565368/ternary-operator-vs-if-else – Martin Perry May 22 '13 at 16:08
  • 1
    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). – Hassedev May 22 '13 at 16:10
  • 1
    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. – Mats Petersson May 22 '13 at 16:13
  • @Hassedev No, the `?:` conditional operator is not allowed to evaluate both of its arguments. – unwind May 22 '13 at 16:24
  • @unwind 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++. – Hassedev May 22 '13 at 16:27
  • 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. – Aki May 23 '13 at 08:30
  • @Aki there is always an implied `else` after an `if` statement, whether you write it or not... – rubenvb May 24 '13 at 13:51
  • @rubenvb If it's just a jmp or a not it doesn't matter as no data is written to memory in the implied else. – Aki May 27 '13 at 08:13

5 Answers5

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).

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • 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. – strcat Feb 14 '15 at 02:43
  • 1
    @strcat 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. – chux - Reinstate Monica Feb 14 '15 at 03:35
  • 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. – strcat Feb 14 '15 at 07:30
  • 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. – strcat Feb 14 '15 at 07:35
  • @strcat 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. – chux - Reinstate Monica Feb 14 '15 at 12:39
  • 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. – strcat Feb 14 '15 at 22:48
  • 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. – strcat Feb 14 '15 at 22:55
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);
}
Yochai Timmer
  • 48,127
  • 24
  • 147
  • 185
  • Where did you learn that the ternary operator is branchless? – martian17 Nov 01 '20 at 23:23
  • 2
    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. – Yochai Timmer Nov 02 '20 at 08:17
  • Ah, now I see that you implemented it yourself! Nice. – martian17 Nov 02 '20 at 09:46
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.

rubenvb
  • 74,642
  • 33
  • 187
  • 332
Hexum2600
  • 149
  • 6
  • 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? – Aki May 23 '13 at 12:24
  • 1
    "don't nest ternary statements"... unless you have no choice (`constexpr` comes to mind). – rubenvb May 24 '13 at 13:53
  • 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. http://en.cppreference.com/w/cpp/language/copy_elision http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ – user666412 Nov 25 '13 at 17:24
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.

Community
  • 1
  • 1
Salgar
  • 7,687
  • 1
  • 25
  • 39
  • 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. – Aki May 23 '13 at 15:46
  • 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. – Salgar May 23 '13 at 15:51
  • 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. – Aki May 24 '13 at 08:20
  • 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. – Spencer Jul 21 '23 at 14:10
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
Chuu
  • 4,301
  • 2
  • 28
  • 54