Is it idiomatic to use `Bool` as `Number`?

Both f and g below do the same thing

f(x) = x ? 0 : 1
g(x) = x * 0 + (!x) * 1

They lower to the same llvm code too:

julia> @code_llvm debuginfo=:none f(true)
define i64 @julia_f_7188(i8 zeroext %0) #0 {
top:
  %1 = and i8 %0, 1
  %2 = xor i8 %1, 1
  %3 = zext i8 %2 to i64
  ret i64 %3
}

julia> @code_llvm debuginfo=:none g(true)
define i64 @julia_g_7190(i8 zeroext %0) #0 {
top:
  %1 = and i8 %0, 1
  %2 = xor i8 %1, 1
  %3 = zext i8 %2 to i64
  ret i64 %3
}

related discussion: Why Bool is a Number?

Sometimes. You can use it as a number and there can be good reasons to do so (like the documented strong zero property of false).

But if you’re just doing simple logical expressions, it’s usually far more readable to other humans to just use the logical operators. The above is just !.

2 Likes

I’m not sure that they always lower to the same thing. Have you tried with Float64 returns? There, it can make the difference between autovectorization kicking in and not. This example though is probably too simple to show any real difference.

1 Like

Neat. I didn’t know about “strong zero”.

Here’s trying with Float64, though yes, this is a simplistic example which is essentially a negation.

ff(x) = x ? 0.0 : 1.0
gg(x) = x * 0.0 + (!x) * 1.0

julia> @code_llvm debuginfo=:none ff(true)
define double @julia_ff_7218(i8 zeroext %0) #0 {
top:
  %1 = and i8 %0, 1
  %.not = icmp eq i8 %1, 0
  %. = select i1 %.not, double 1.000000e+00, double 0.000000e+00
  ret double %.
}

julia> @code_llvm debuginfo=:none gg(true)
define double @julia_gg_7220(i8 zeroext %0) #0 {
top:
  %1 = and i8 %0, 1
  %.not.not = icmp eq i8 %1, 0
  %2 = select i1 %.not.not, double 1.000000e+00, double 0.000000e+00
  ret double %2
}

The llvm is not exactly the same. I don’t know if it’s meaningfully different. I don’t really know how to read llvm.

I’ve heard people express concerns about excessive branching in if/else. I don’t understand what’s exactly sub-optimal about it. I thought that, if what’s behind the if/else conditions are numerical computations, using Bool values directly in those computations could be one way to get rid of branching. Though in simple cases like these, the compiler seems to do just fine with if/else.

Typically the compiler is smart enough these days to remove the branches if they’re not needed, just like you see above. The branchy version often gives the compiler more latitude to do what it thinks will be best. And branches — predictable ones — are often near-zero cost.

Branches do prevent SIMD and unpredictable branches can incur some fairly big slowdowns, so that’s where this becomes more important.

Write for humans first. Then profile and optimize if necessary.

5 Likes