Description
Using the ternary operator with constants creates a lot of unnecessary branches and jumps. It can be factored into a common base and based on the boolean result, the difference can be multiplied with the condition state and then added -- or a "-1" mask (negated state) on the difference can be used.
clang, gcc, msvc all do that.
publicintValue(boolcond)=>cond?c_ConstA:c_ConstB;
It's a common code pattern to return 2 different constants based on a flag, on a condition, etc. Like makeUpper ? 'A' : 'a'
publicintValue(boolcond)=>c_ConstA+cond*(c_ConstB-c_ConstA);
publicintValue(boolcond)=>c_ConstA+(-cond&(c_ConstB-c_ConstA));
Note: C# cannot easily convert a boolean into an integer (0/1) but the JIT will have no issue with that.
Configuration
.NET 6.0.0 (6.0.21.52210)
Regression?
No
Data
Analysis
[Benchmark]publicintA()=>Cond?171:239;[Benchmark]publicintB()=>171+CondInt*(239-171);volatileboolCond;volatileintCondInt;// bool as int as there is no friction-free C# conversion
.NET 6.0.0 (6.0.21.52210), X64 RyuJIT
; Benchmark+Bench.A() 7FFB738AD610 cmp byte ptr [rcx+18],0 7FFB738AD614 jne short M00_L00 7FFB738AD616 moveax,0EF 7FFB738AD61B retM00_L00: 7FFB738AD61C moveax,0AB 7FFB738AD621 ret; Total bytes of code 18
.NET 6.0.0 (6.0.21.52210), X64 RyuJIT
; Benchmark+Bench.B() 7FFB7389C4E0 imuleax,[rcx+14],44 7FFB7389C4E4 addeax,0AB 7FFB7389C4E9 ret; Total bytes of code 10
I did some benchmarking, but added a 1000-loop to reduce the call-frame jitter, and the difference is significant. And that doesn't even include branch misprediction which real-world code will have at around 50%. Here, misprediction is 0% as the code is too artificial and predictable. Nonetheless, it's twice as fast and with bad branch prediction it will be an order of magnitude when the 18-cycle penalty has an effect.
[Benchmark]publicboolA(){for(varnn=0;nn<1000;++nn)Value=Cond?171:239;returntrue;}[Benchmark]publicboolB(){for(varnn=0;nn<1000;++nn)Value=171+CondI*(239-171);returntrue;}volatileintValue;volatileboolCond;volatileintCondI;| Method | Mean | Error | StdDev | Code Size |
|------- |---------:|--------:|--------:|----------:|
| A | 756.3 ns | 2.37 ns | 2.22 ns | 41 B |
| B | 434.1 ns | 0.26 ns | 0.24 ns | 30 B |
Description
Using the ternary operator with constants creates a lot of unnecessary branches and jumps. It can be factored into a common base and based on the boolean result, the difference can be multiplied with the condition state and then added -- or a "-1" mask (negated state) on the difference can be used.
clang, gcc, msvc all do that.
It's a common code pattern to return 2 different constants based on a flag, on a condition, etc. Like
makeUpper ? 'A' : 'a'Note: C# cannot easily convert a boolean into an integer (0/1) but the JIT will have no issue with that.
Configuration
.NET 6.0.0 (6.0.21.52210)
Regression?
No
Data
Analysis
.NET 6.0.0 (6.0.21.52210), X64 RyuJIT
.NET 6.0.0 (6.0.21.52210), X64 RyuJIT
I did some benchmarking, but added a 1000-loop to reduce the call-frame jitter, and the difference is significant. And that doesn't even include branch misprediction which real-world code will have at around 50%. Here, misprediction is 0% as the code is too artificial and predictable. Nonetheless, it's twice as fast and with bad branch prediction it will be an order of magnitude when the 18-cycle penalty has an effect.