Подтвердить что ты не робот

Почему медленнее сравнивать тип значения NULL с нулевым значением для общего метода без ограничений?

Я столкнулся с очень забавной ситуацией, когда сравнение типа nullable с null внутри общего метода на 234x медленнее, чем сравнение типа значения или ссылочного типа. Код выглядит следующим образом:

static bool IsNull<T>(T instance)
{
    return instance == null;
}

Код выполнения:

int? a = 0;
string b = "A";
int c = 0;

var watch = Stopwatch.StartNew();

for (int i = 0; i < 1000000; i++)
{
    var r1 = IsNull(a);
}

Console.WriteLine(watch.Elapsed.ToString());

watch.Restart();

for (int i = 0; i < 1000000; i++)
{
    var r2 = IsNull(b);
}

Console.WriteLine(watch.Elapsed.ToString());

watch.Restart();

for (int i = 0; i < 1000000; i++)
{
    var r3 = IsNull(c);
}

watch.Stop();

Console.WriteLine(watch.Elapsed.ToString());
Console.ReadKey();

Вывод для кода выше:

00: 00: 00,1879827

00: 00: 00,0008779

00: 00: 00,0008532

Как вы можете видеть, сравнение значения nullable int в null равно 234x медленнее, чем сравнение int или строки. Если я добавлю вторую перегрузку с правильными ограничениями, результаты резко меняются:

static bool IsNull<T>(T? instance) where T : struct
{
    return instance == null;
}

Теперь результаты:

00: 00: 00,0006040

00: 00: 00,0006017

00: 00: 00,0006014

Почему? Я не проверял байтовый код, потому что я не уверен в этом, но даже если бы байтовый код был немного другим, я бы ожидал, что JIT оптимизирует это, и это не (я работаю с оптимизацией).

4b9b3361

Ответ 1

Если вы сравните IL, произведенный двумя перегрузками, вы увидите, что есть бокс:

Первое выглядит так:

.method private hidebysig static bool IsNull<T>(!!T instance) cil managed
{
    .maxstack 2
    .locals init (
        [0] bool CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: box !!T
    L_0007: ldnull 
    L_0008: ceq 
    L_000a: stloc.0 
    L_000b: br.s L_000d
    L_000d: ldloc.0 
    L_000e: ret 
}

Пока второй выглядит следующим образом:

.method private hidebysig static bool IsNull<valuetype ([mscorlib]System.ValueType) .ctor T>(valuetype [mscorlib]System.Nullable`1<!!T> instance) cil managed
{
    .maxstack 2
    .locals init (
        [0] bool CS$1$0000)
    L_0000: nop 
    L_0001: ldarga.s instance
    L_0003: call instance bool [mscorlib]System.Nullable`1<!!T>::get_HasValue()
    L_0008: ldc.i4.0 
    L_0009: ceq 
    L_000b: stloc.0 
    L_000c: br.s L_000e
    L_000e: ldloc.0 
    L_000f: ret 
}

Во втором случае компилятор знает, что тип является Nullable, поэтому он может оптимизировать для этого. В первом случае он должен обрабатывать любые типы, как ссылочные, так и типы значений. Поэтому он должен прыгать через дополнительные обручи.

Что касается того, почему int быстрее, чем int?, я бы предположил, что там есть некоторые оптимизации JIT.

Ответ 2

Вот что вы должны сделать, чтобы исследовать это.

Начните с перезаписи программы, чтобы она делала все дважды. Поместите окно сообщения между двумя итерациями. Скомпилируйте программу с оптимизацией и запустите программу не в отладчике. Это гарантирует, что дрожание генерирует наиболее оптимальный код, который он может использовать. Джиттер знает, когда отладчик подключен и может генерировать худший код, чтобы облегчить отладку, если он думает, что вы делаете.

Когда появится окно сообщения, присоедините отладчик, а затем выполните трассировку на уровне кода сборки в трех разных версиях кода, если на самом деле есть три разные версии. Я бы поставил столько же, сколько доллар, который не генерирует никакого кода для первого, потому что джиттер знает, что все это можно оптимизировать, чтобы "вернуть ложь", а затем вернуть значение false, и, возможно, даже цикл можно удалить.

(В будущем вы, вероятно, должны учитывать это при написании тестов производительности. Помните, что если вы не используете результат, то дрожание может полностью оптимизировать все, что создает этот результат, если у него нет стороны эффект.)

После просмотра кода сборки вы увидите, что происходит.

Я сам этого не исследовал лично, но все хорошо, что происходит:

  • в int codepath, дрожание понимает, что boxed int никогда не является нулевым и превращает метод в "return false"

  • в кодировке строк, дрожание понимает, что тестирование строки для недействительности эквивалентно проверке того, равен ли управляемый указатель на строку, поэтому он генерирует одну команду, которая проверяет, равен ли регистр нулю.

  • в int? codepath, возможно, дрожание понимает, что тестирование int? для недействительности может быть выполнено путем бокса int? - поскольку нулевое значение null в ячейке является нулевой ссылкой, что затем сводится к более ранней задаче проверки управляемого указателя на нуль. Но вы берете на себя стоимость бокса.

Если это случай, то дрожание может быть более сложным здесь и понять, что тестирование int? для null может быть выполнено, возвращая обратное значение HasValue bool внутри int.

Но, как я уже сказал, это просто догадка. Создайте код самостоятельно и посмотрите, что он делает, если вам интересно.

Ответ 3

Бокс и распаковка происходят там, не зная об этом, и боксерские операции, как известно, медленны. Это потому, что вы в фоновом режиме конвертируете нулевые ссылочные типы в типы значений.