-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Optimization: left ?? right
can be optimized to left.GetValueOfDefault()
when appropriate
#22800
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This came up in a Gitter discussion today about the three ways to tell if a
Currently 1 and 3 are similar, and the work in features/recursive-patterns makes 2 similar to the existing ones. The change proposed here would give a clear advantage to 3. That's not necessarily bad, but I also wouldn't mind seeing all three cases handled as part of the work here.
@gafter @jaredpar would need to weigh in here, but the benchmark numbers I saw today were compelling. |
I think codegen for lifted comparison operators should be unified somehow, there's no reason for |
Perhaps we could all see them? 😄 |
@jaredpar Sam is referring to performance tables tables starting at https://gitter.im/dotnet/csharplang?at=5b192e4535e25f399757c1e0. |
@jnm2 where is the code for the benchmarks? That is more important than the resulting tables. |
@jaredpar The source code is near each table of results. Sam just asked for a new one, so here it all is: Code (project targets net472 and netcoreapp2.1): [ClrJob, CoreJob]
public class Benchmarks
{
private static readonly Random Random = new Random();
private static readonly bool?[] Values = { null, false, true };
private static bool? GetValue() => Values[Random.Next(0, 3)];
[Benchmark]
public bool Coalesce() => GetValue() ?? false;
[Benchmark]
public bool Compare() => GetValue() == true;
[Benchmark]
public bool GetValueOrDefault() => GetValue().GetValueOrDefault();
[Benchmark]
public void Noop() => GetValue();
}
} Running on i7-7700K with Windows 10.0.17134.48:
|
Ideally BenchmarkDotNet would let you inject different arguments during the same run. The only way to get the Random.Next and lookup overhead out is either subtraction after the fact, or test each input in a separate run and see branch prediction play out. |
Subtracting the no-op mean from the other rows' means and adding the no-op error to the other row's errors:
This seems to indicate that GetValueOrDefault brings a 3.96× speed increase on .NET Core 2.1 and a 4.17× increase on .NET Framework 4.7.2. |
Here's a comparison without random: using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Running;
namespace Benchmarks
{
public static class Program
{
public static void Main()
{
BenchmarkRunner.Run<Benchmarks>();
}
}
[ClrJob, CoreJob]
public class Benchmarks
{
[Params(null, false, true)]
public bool? Value;
[Benchmark]
public bool Coalesce() => Value ?? false;
[Benchmark]
public bool Compare() => Value == true;
[Benchmark]
public bool PatternOld() => Value is true; // code as being generated by C# 7.3
[Benchmark]
public bool PatternNew() // code as being generated by C# 8 recursive patterns branch
{
var x = Value;
if (x.HasValue)
{
return x.GetValueOrDefault();
}
return false;
}
[Benchmark]
public bool GetOrDefault() => Value.GetValueOrDefault();
}
} BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17134
Intel Core i5-4670K CPU 3.40GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.1.300
[Host] : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
Clr : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3110.0
Core : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
|
I can open a first PR for what's this issue about (lower I think optimizing |
The fix was merged into features/compiler for 16.0. |
I suggested something like this in #56007, as the current implementation generates an unnecessary temporary for value types. |
While writing some code the other day, I wondered if, for a variable
x
of typeint?
, whetherx ?? 0
andx.GetValueOrDefault()
are really compiled to the same IL (I first assumed that they are). To my surprise, they aren't!GetValueOrDefault
is faster because it's implemented as a single unconditional field load. You can't really get much simpler than that, and it's a great candidate for inlining.While it's an implementation detail, Roslyn already relies on it: in some cases where it's known that a nullable expression has a value, the compiler generates
nullable.GetValueOrDefault()
to access the value rather than usingnullable.Value
.The change seemed simple enough: lower any
left ?? right
toleft.GetValueOrDefault()
when left isT?
and right is the default value ofT
. I've wanted to poke around the Roslyn codebase for a while, so here was my chance :)Here's the commit implementing this optimization along with a test: https://github.com/MrJul/roslyn/commit/ed21c4d8b025ec203a474e814c91ec701c6ac59f
Is it worth it? While it's a micro optimization, it's still an optimization (BenchmarkDotNet shows a ~60% improvement with RyuJIT x64 and .NET 4.7 on my machine - we're talking about sub-nanoseconds times here), and a pretty straightforward one in my opinion.
I didn't handle the case
left ?? right
whenleft
is aT?
andright
is a non-defaultT
constant sinceGetValueOrDefault(T)
(with a parameter) has branches equivalent to what the compiler generates so the only benefit is a very slight reduction in IL size (which might still help with inlining). However, I can add it if needed.If the team is interested in this change, I'll open a PR.
The text was updated successfully, but these errors were encountered: