Я проверил на 10 млн объектов и получилось что foo is null в 20 раз быстрее чем foo == null. Поэтому и интересно какой способ самый быстрый.
Вы же в курсе, как нужно проводить тесты на производительность ?)
foo == null, foo is null, ReferenceEquals(foo, null) - все они компилируются в одно и то же, по тому разницы никакой нет.
Пояснение
Вот код:
public class Benchmark
{
private static readonly object? Obj = new();
[Benchmark]
public bool EqualityOperator() => Obj == null;
[Benchmark]
public bool PatternMatching() => Obj is null;
[Benchmark] public bool ComplexPatterMatching() => Obj is not { };
[Benchmark] public bool ConstantReturn() => false;
[Benchmark] public bool EqualsCall() => Obj!.Equals(null);
[Benchmark] public bool ReferenceEqualsCall() => ReferenceEquals(Obj, null);
}
Вот IL:
.method public hidebysig instance bool
EqualityOperator() cil managed
{
.custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
// [10 39 - 10 50]
IL_0000: ldsfld object Benchmark::Obj
IL_0005: ldnull
IL_0006: ceq
IL_0008: ret
} // end of method Benchmark::EqualityOperator
.method public hidebysig instance bool
PatternMatching() cil managed
{
.custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
// [12 38 - 12 49]
IL_0000: ldsfld object Benchmark::Obj
IL_0005: ldnull
IL_0006: ceq
IL_0008: ret
} // end of method Benchmark::PatternMatching
.method public hidebysig instance bool
ComplexPatterMatching() cil managed
{
.custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
// [13 56 - 13 70]
IL_0000: ldsfld object Benchmark::Obj
IL_0005: ldnull
IL_0006: cgt.un
IL_0008: ldc.i4.0
IL_0009: ceq
IL_000b: ret
} // end of method Benchmark::ComplexPatterMatching
.method public hidebysig instance bool
EqualsCall() cil managed
{
.custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
// [15 45 - 15 62]
IL_0000: ldsfld object Benchmark::Obj
IL_0005: ldnull
IL_0006: callvirt instance bool [System.Runtime]System.Object::Equals(object)
IL_000b: ret
} // end of method Benchmark::EqualsCall
.method public hidebysig instance bool
ReferenceEqualsCall() cil managed
{
.custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor()
= (01 00 00 00 )
.maxstack 8
// [16 54 - 16 80]
IL_0000: ldsfld object Benchmark::Obj
IL_0005: ldnull
IL_0006: ceq
IL_0008: ret
} // end of method Benchmark::ReferenceEqualsCall
Тоесть в теории всё должно быть одинаково, кроме ReferenceEquals и ComplexPatternMatching. Но есть же ещё JIT и PGO. (надеюсь, что они не испортят результаты теста, и не превратят сравнение в константу)
Блин, таки превратил в константу
| Method | Mean | Error | StdDev | Median |
|---------------------- |----------:|----------:|----------:|----------:|
| EqualityOperator | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns |
| PatternMatching | 0.0300 ns | 0.0164 ns | 0.0145 ns | 0.0271 ns |
| ComplexPatterMatching | 0.0401 ns | 0.0327 ns | 0.0376 ns | 0.0267 ns |
| ConstantReturn | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns |
| EqualsCall | 1.3787 ns | 0.0437 ns | 0.0409 ns | 1.3770 ns |
| ReferenceEqualsCall | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.0000 ns |
Вот нормальный результат бенчмарка после засовывания поля в свойство и запрета на инлайнинг и оптимизацию этого свойства:
| Method | Mean | Error | StdDev |
|---------------------- |----------:|----------:|----------:|
| EqualityOperator | 2.5751 ns | 0.0062 ns | 0.0049 ns |
| PatternMatching | 2.5682 ns | 0.0073 ns | 0.0065 ns |
| ComplexPatterMatching | 2.6456 ns | 0.0744 ns | 0.0696 ns |
| ConstantReturn | 0.0065 ns | 0.0044 ns | 0.0035 ns |
| EqualsCall | 4.6958 ns | 0.0337 ns | 0.0282 ns |
| ReferenceEqualsCall | 2.9525 ns | 0.0667 ns | 0.0557 ns |
Тоесть все различия на уровне погрешности.
А вот и JITAsm:
sharplab
PS: результаты бенчмарка не актуальны для Nullable, но там должно быть аналогично - только ReferenceEquals не стоит звать, и хз что будет при is not {}
Так что я бы использовал is null либо == null
PPS: если используется юнити - то там тоже не очень актуально, тк в юнити другой JIT, нет PGO, да и вообще может использовать IL2CPP
PPPS: тут ещё подсказывают, что == может иметь неожиданное поведение из-за перегрузок операторов.