P
P
Pragma Games2021-12-10 14:31:18
C++ / C#
Pragma Games, 2021-12-10 14:31:18

What is the fastest null check?

Hello. In my project, checks for null often occur, tell me which one is the fastest? I am aware of several options: foo == null, foo is null, foo == false, Object.Equals(foo, null)

Answer the question

In order to leave comments, you need to log in

2 answer(s)
V
Vasily Bannikov, 2021-12-10
@PragmaGames

I checked for 10 million objects and it turned out that foo is null is 20 times faster than foo == null. Therefore, I wonder which way is the fastest.

Do you know how to conduct performance tests?)
foo == null, foo is null, ReferenceEquals(foo, null) - they all compile to the same thing, so there is no difference.
Explanation

Вот код:
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: the benchmark results are not relevant for Nullable, but it should be similar there - only ReferenceEquals should not be called, and xs what will happen if is not {}
So I would use is null or == null
PPS: if a unit is used, then there too not very relevant, since the unit has a different JIT, there is no PGO, and in general it can use IL2CPP PPPS
: they also suggest that == may have unexpected behavior due to operator overloads.

A
AndromedaStar, 2021-12-10
@AndromedaStar

All checks for null after compilation are two assembler instructions. So they are all extremely fast.
Well, except for Object.Equals(foo, null), since you need to compare Object.ReferenceEquals(foo, null)

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question