# Ifelse vs ternary with tuples

**URL:** <https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423>\
**Category:** General Usage\
**Tags:** performance, memory-allocation\
**Created:** [February 19, 2024, 9:42pm UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423 "2024-02-19T21:42:10Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![moble](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/moble/32/23535_2.png) [@moble](https://discourse.julialang.org/u/moble)\
**Post date:** [February 19, 2024, 9:42pm UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423/1 "2024-02-19T21:42:10Z")

</div>

I believe I have the basic idea of when to use `ifelse` instead of the ternary operator. Basically, `ifelse` avoids an expensive branch, but computes the two possibilities — which may be more expensive. (Also `ifelse` may be friendler to autodiff.) But now throw tuples into the mix, and I have questions. Specifically, looking at [these lines in the Julia source](https://github.com/JuliaLang/julia/blob/02699bb6cd83d16b1e51e2fb127241e18df7c56b/base/promotion.jl#L646-L648):

```julia
max(x::T, y::T) where {T<:Real} = ifelse(y < x, x, y)
min(x::T, y::T) where {T<:Real} = ifelse(y < x, y, x)
minmax(x::T, y::T) where {T<:Real} = y < x ? (y, x) : (x, y)

```

For `min` and `max`, there’s nothing to compute between `x` and `y`, so just use `ifelse`. But for `minmax`, would those tuples actually get created? I have this very fuzzy notion that tuples are basically free, and the compiler knows how to deal with them very well. So, is this code saying they’re not that free?

For example, if `x` and `y` are `BigFloat`s (which do require allocation), would `max` and `min` get away without allocating somehow? Would `minmax` have to allocate four `BigFloat`s to fill the two tuples, and then just return one of the tuples?

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [February 19, 2024, 9:53pm UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423/2 "2024-02-19T21:53:43Z")

</div>

Things get a little confused because the compiler itself can — and does — do transformations from `?:` to `ifelse` (edit: and vice-versa!) when it can prove it’s advantageous (and ok) to do so. And it’s even more confused because branches aren’t necessarily bad (if they’re predictable and don’t mess up SIMD).

Those lines of code are pretty old, too — 10+ years — so I’m not sure if they’re necessarily instructive or optimal either.

In fact, Julia v1.11 (but not v1.10) compiles that minmax to the branchless select.

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [February 19, 2024, 10:08pm UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423/3 "2024-02-19T22:08:27Z")

</div>

I think the best guidance these days is to just use `?:` branches and let the compiler handle it unless you’re hand-crafting SIMD kernels or wanting the convenience (and, e.g., broadcast-ability) of a function.

---

<div class="post-metadata">

**Author:** ![mikmoore](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mikmoore/32/31109_2.png) [@mikmoore](https://discourse.julialang.org/u/mikmoore)\
**Post date:** [February 19, 2024, 10:14pm UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423/4 "2024-02-19T22:14:10Z")

</div>

The above poster beat me to most of this answer, but I think this still adds something so I’m posting it:

The compiler gets its hands on any of these and does what it wants (within the range it’s allowed). It’s common that ternary expressions get compiled to be branchless and also that a nontrivial `ifelse` uses a branch.

There is a semantic difference in that `ifelse` must always execute both arguments and `?:` must only execute the correct side, but this distinction only matters when either has side-effects. For example, `ifelse(true, 1, error())` must error while `true ? 1 : error()` must not.

In general, it tends to be slightly easier for the compiler to make branch-free code using `ifelse` rather than `?:`. This is why it is desirable when one is attempting to write vectorizable code for hot loops. However, `?:` technically allows the compiler more freedom in most situations where `ifelse` is relevant because it is allowed to _not_ evaluate one side entirely (including side effects).

Since forming a tuple from two objects does not have side effects, it’s likely that either a `?:` or `ifelse` implementation of the functions in the original question result in the same compiled code. `code_native` is useful here.

```julia-repl
julia> code_native((x,y) -> y < x ? (y, x) : (x, y), NTuple{2,Float64}; debuginfo=:none)

julia> code_native((x,y) -> ifelse(y < x, (y, x), (x, y)), NTuple{2,Float64}; debuginfo=:none)

```

On my x86 running Julia v1.10, I find the assembly of the `ifelse` more attractive in this case (the ternary does emit a branch while the `ifelse` does not). However, changing the comparison to `y <= x` emits branches for both versions and I slightly prefer the ternary.

Keep in mind that a function this small is likely to be inlined, which means that it may interact with other code in significant ways. This means that this is probably not the end of the story. For example, in a loop the compiler may eliminate the branch in order to enable vectorization.

---

<div class="post-metadata">

**Author:** ![moble](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/moble/32/23535_2.png) [@moble](https://discourse.julialang.org/u/moble)\
**Post date:** [February 20, 2024, 1:17am UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423/5 "2024-02-20T01:17:04Z")

</div>

Thanks for the explanations. I guess, as usual, the way to go is to just write code that reads well, and don’t worry about details like this until a profiler says to — then experiment to see what the compiler actually does.

---

<div class="post-metadata">

**Author:** ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)\
**Post date:** [February 21, 2024, 4:36am UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423/6 "2024-02-21T04:36:26Z")

</div>

Note that the compiler tends to prefer using branches, under the assumption that well predicted code.

One example where this backfires is in binary searches, which are generally impossible to predict.  
I get

```julia
julia> using Random, BenchmarkTools

julia> x = rand(Int, 2048); s = sort(x);

julia> perm = randperm(length(x));

julia> function findbench(f, x, vals)
           @inbounds for i = eachindex(x, vals)
               v = vals[i]
               f(x[v], x) == v || throw("oops")
           end
       end
findbench (generic function with 1 method)

julia> @benchmark findbench((x,v)->searchsortedfirst(v, x), $s, $perm)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max): 94.526 μs … 117.188 μs ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 95.964 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 96.209 μs ± 1.087 μs ┊ GC (mean ± σ): 0.00% ± 0.00%

            ▁▂▄▄▆▇█▇▆▆▄▂                                        
  ▂▂▂▂▂▃▄▄▆▇█████████████▆▅▄▄▃▃▂▂▂▂▂▂▂▁▂▂▂▂▂▂▃▃▃▃▃▃▃▃▃▃▃▃▃▃▃▂▂ ▄
  94.5 μs Histogram: frequency by time 99.6 μs <

 Memory estimate: 0 bytes, allocs estimate: 0.

```

However, if I start Julia via `JULIA_LLVM_ARGS="-x86-cmov-converter=false" julia`, which disables the cmov-\>branch conversion (so our code is actually branchless), I get

```julia
julia> @benchmark findbench((x,v)->searchsortedfirst(v, x), $s, $perm)
BenchmarkTools.Trial: 10000 samples with 1 evaluation.
 Range (min … max): 40.656 μs … 62.645 μs ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 40.919 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 41.682 μs ± 2.223 μs ┊ GC (mean ± σ): 0.00% ± 0.00%

  ▄█▆ ▁▁▁ ▂▂▁ ▁▂▁ ▁
  ███▇▃▁▁▃▁▁▃▁▁▁▁▃▄▆▇██████▆▅▄▃▁▄▄▄▄▁▄▃▆███▆▁▁▁▃▁▁▁▁▅████▆▅▅▆ █
  40.7 μs Histogram: log(frequency) by time 49.9 μs <

 Memory estimate: 0 bytes, allocs estimate: 0.

```

Would be nice if we had a way to tell the compiler “no really, I do want this particular code to be branchless!” without using a hammer that impacts all code like that `ENV` variable.

---

<div class="post-metadata">

**Author:** ![AMJ](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/amj/32/214096_2.png) [@AMJ](https://discourse.julialang.org/u/AMJ)\
**Post date:** [February 21, 2024, 9:03am UTC](https://discourse.julialang.org/t/ifelse-vs-ternary-with-tuples/110423/7 "2024-02-21T09:03:02Z")

</div>

I didn’t know we could do this! But yeah it would be really nice if we could tell which parts do branchless/branchful.

I did implement [one](https://julialang.slack.com/archives/C029LM0R5B3/p1704445220058919?thread_ts=1704202961.938839&cid=C029LM0R5B3) with `@asmcall` when I was trying to learn more about asm, the performance is comparable. Although, you start to get worse performance if your array does not fit L1/L2 cache.
