В дополнение к серии на тему различных WTF, начатаю ув. Sinix
Итак. Даны 2 примера кода:
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetNumber_1(object value)
{
var v = value as int?;
if (v != null)
return v.GetValueOrDefault();
return 0;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetNumber_2(object value)
{
if (value is int)
return (int)value;
return 0;
}
Что здесь не так? Оба кода делают одно и тоже — возвращают забоксенное значение в случае, если там int и 0 в противном случае.
Код для проверки предположений (запускать без отладчика, Ctrl-F5):
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace ConsoleApplication1
{
public static class Program
{
public const int Count = 100 * 1000 * 1000;
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetNumber_1(object value)
{
var v = value as int?;
if (v != null)
return v.GetValueOrDefault();
return 0;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetNumber_2(object value)
{
if (value is int)
return (int)value;
return 0;
}
public static void Main()
{
var a = (object)0;
GetNumber_1(a);
GetNumber_2(a);
Measure("Get number via: as int?", () =>
{
var x = a;
for (var i = 0; i < Count; i++)
{
GetNumber_1(x);
}
});
Measure("Get number via: is int ", () =>
{
var x = a;
for (var i = 0; i < Count; i++)
{
GetNumber_2(x);
}
});
}
private static void Measure(string name, Action action)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var sw = Stopwatch.StartNew();
action();
sw.Stop();
Console.WriteLine("{0,20}: {1,5} ms, ips: {2,15:N}", name, sw.ElapsedMilliseconds, Count / sw.Elapsed.TotalSeconds);
}
}
}
Здравствуйте, rameel, Вы писали:
R>Всем привет!
R>В дополнение к серии на тему различных WTF, начатаю ув. Sinix
R>Итак. Даны 2 примера кода:
R>
R>[MethodImpl(MethodImplOptions.NoInlining)]
R>public static int GetNumber_1(object value)
R>{
R> var v = value as int?;
R> if (v != null)
R> return v.GetValueOrDefault();
R> return 0;
R>}
R>[MethodImpl(MethodImplOptions.NoInlining)]
R>public static int GetNumber_2(object value)
R>{
R> if (value is int)
R> return (int)value;
R> return 0;
R>}
R>
R>Что здесь не так? Оба кода делают одно и тоже — возвращают забоксенное значение в случае, если там int и 0 в противном случае.
Я правильно понимаю, что грабли зарыты в value as int?; ?
Судя по всему там анбокс до инта, потом создание nullable<int>, потом приведение к нему.
Здравствуйте, Codechanger, Вы писали:
C>Я правильно понимаю, что грабли зарыты в value as int?; ?
C>Судя по всему там анбокс до инта, потом создание nullable<int>, потом приведение к нему.
Здравствуйте, namespace, Вы писали:
N>Создается структура для хранения nullable. Но почему хранит в куче — вопрос интересный.
Не в куче, посмотрите на disassembly.
Для object value = 0;
-------------------------------------------------------
Get number via: as int?: 2252 ms, ips: 44 391 565,21
Get number via: is int : 196 ms, ips: 507 638 693,24
Для object value = null;
-------------------------------------------------------
Get number via: as int?: 1378 ms, ips: 72 535 578,34
Get number via: is int : 182 ms, ips: 548 246 515,76
Честно говоря, я не ожидал увидеть 10 кратную просадку в производительности с приведением к Nullable
В случае is int проверяется, что объект не null и что он соответствует проверяемому типу. Приведение к типу тоже проверяет, что объект не null и что он соответствует проверяемому типу, и в случае успеха просто достает значение из памяти без каких-либо дополнительных телодвижений, в противном случае генерируется вызов CORINFO_HELP_UNBOX, который умеет в частности доставать underlyng type в случае enum.
С Nullable же дело обстоит иначе. Так как забоксенное значение отличается по структуре (memory layout) от Nullable, то джит генерирует вызовы 2 методов: CORINFO_HELP_ISINSTANCEOFANY, который проверяет, что забоксенное значение соответствует типу, который завернут в Nullable, и CORINFO_HELP_UNBOX_NULLABLE, который собственно и занимается преобразованием в структуру Nullable<T>.
Как это выглядит для GetNumber_2:
Текущий джит для проверки через is с последующим кастом генерирует одни и те же проверки 2 раза, хотя мог бы сделать это 1 раз Что ж, весьма последовательно
if (value is int)
return (int)value;
return 0;
00007FF7BA0C0670 push rsi
00007FF7BA0C0671 sub rsp,20h
00007FF7BA0C0675 mov rsi,rcx
00007FF7BA0C0678 mov rdx,rsi
; Проверка на null
00007FF7BA0D067B test rdx,rdx
00007FF7BA0D067E je 00007FF7BA0D0691
; Проверка на соответствие типа
00007FF7BA0D0680 mov rcx,7FF818853E98h
00007FF7BA0D068A cmp qword ptr [rdx],rcx
00007FF7BA0D068D je 00007FF7BA0D0691
00007FF7BA0D068F xor edx,edx
; Проверка на null
00007FF7BA0D0691 test rdx,rdx
00007FF7BA0D0694 je 00007FF7BA0D06C0
; Проверка на соответствие типа
00007FF7BA0D0696 mov rdx,7FF818853E98h
00007FF7BA0D06A0 cmp qword ptr [rsi],rdx
00007FF7BA0D06A3 je 00007FF7BA0D06B7
; Вызов метода CORINFO_HELP_UNBOX
00007FF7BA0D06A5 mov rdx,rsi
00007FF7BA0D06A8 mov rcx,7FF818853E98h
00007FF7BA0D06B2 call 00007FF819757EB0
; Приведение типа - просто достаем значение напрямую из памяти
00007FF7BA0D06B7 mov eax,dword ptr [rsi+8]
00007FF7BA0D06BA add rsp,20h
00007FF7BA0D06BE pop rsi
00007FF7BA0D06BF ret
; Возвращаем 0
00007FF7BA0D06C0 xor eax,eax
00007FF7BA0D06C2 add rsp,20h
00007FF7BA0D06C6 pop rsi
00007FF7BA0D06C7 ret
Как это выглядит для GetNumber_1:
var v = value as int?;
if (v != null)
return v.GetValueOrDefault();
return 0;
public int GetNumber(object value)
{
int? num = value as int?;
int valueOrDefault = num.GetValueOrDefault();
if (num.HasValue)
{
return valueOrDefault;
}
return 0;
}