PragmaGames
@PragmaGames
Увлекаюсь Unity.

Какая самая быстрая проверка на null?

Всем привет. В моем проекте часто происходят проверки на null, подскажите какая из них самая быстра ? Я знаю о нескольких вариантах : foo == null, foo is null, foo == false, Object.Equals(foo, null)
  • Вопрос задан
  • 293 просмотра
Решения вопроса 1
Я проверил на 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: тут ещё подсказывают, что == может иметь неожиданное поведение из-за перегрузок операторов.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
@AndromedaStar
.Net - monkey
Все проверки на null после компиляции представляют собой две команды ассемблера. Так что они все крайне быстрые.
Ну кроме Object.Equals(foo, null), так как нужно сравнивать Object.ReferenceEquals(foo, null)
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы