Почему медленнее сравнивать тип значения Nullable со значением NULL в универсальном методе без ограничений? - PullRequest
13 голосов
/ 18 апреля 2011

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

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

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

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 оптимизирует это, а это не так (я работаю с оптимизацией) .

Ответы [ 3 ]

14 голосов
/ 19 апреля 2011

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

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

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

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

Я лично не исследовал это лично, но есть вероятность, что происходит следующее:

  • в коде int, джиттер понимает, что в штучной упаковкеint никогда не имеет значения null и превращая метод в «return false»

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

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

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

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

5 голосов
/ 19 апреля 2011

Если вы сравните 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-оптимизации.

3 голосов
/ 18 апреля 2011

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

...