Минутка WTF-12: из реального проекта
От: rameel https://github.com/rsdn/CodeJam
Дата: 03.11.16 10:35
Оценка: 56 (2) :)
Всем привет!

В дополнение к серии на тему различных 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);
        }
    }
}
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Отредактировано 08.01.2017 13:38 rameel . Предыдущая версия .
минутка wtf
Re: Минутка WTF-12: из реального проекта
От: Codechanger Россия  
Дата: 03.11.16 13:06
Оценка:
Здравствуйте, 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>, потом приведение к нему.
Re[2]: Минутка WTF-12: из реального проекта
От: Sharov Россия  
Дата: 03.11.16 13:48
Оценка:
Здравствуйте, Codechanger, Вы писали:

C>Я правильно понимаю, что грабли зарыты в value as int?; ?


C>Судя по всему там анбокс до инта, потом создание nullable<int>, потом приведение к нему.


Мне кажется здесь грабель
if (v != null)

попытка проверить, есть значение или нет.
Кодом людям нужно помогать!
Re[3]: Минутка WTF-12: из реального проекта
От: TK Лес кывт.рф
Дата: 04.11.16 09:26
Оценка: +1
Здравствуйте, Sharov, Вы писали:

S>Мне кажется здесь грабель

S> if (v != null)

S>попытка проверить, есть значение или нет.


Проверять на null вообще смысла нет. можно сразу звать GetValueOrDefault()
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re: Минутка WTF-12: из реального проекта
От: namespace  
Дата: 05.11.16 09:27
Оценка:
R>Итак. Даны 2 примера кода
var v = value as int?;

Создается структура для хранения nullable. Но почему хранит в куче — вопрос интересный.

Можно так написать:
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetNumber_1(object value)
{
    if(value == null)
        return 0;

    return (int)value;
}
Re[2]: Минутка WTF-12: из реального проекта
От: Sinix  
Дата: 05.11.16 09:34
Оценка: +2
Здравствуйте, namespace, Вы писали:

N>Создается структура для хранения nullable. Но почему хранит в куче — вопрос интересный.

Не в куче, посмотрите на disassembly.
Re[2]: Минутка WTF-12: из реального проекта
От: TK Лес кывт.рф
Дата: 05.11.16 11:10
Оценка: +2
Здравствуйте, namespace, Вы писали:

N>Можно так написать:


Нельзя — код не равнозначный
Если у Вас нет паранойи, то это еще не значит, что они за Вами не следят.
Re: Минутка WTF-12: из реального проекта
От: rameel https://github.com/rsdn/CodeJam
Дата: 06.11.16 14:37
Оценка: 80 (2) +1
Здравствуйте, rameel, Вы писали:

Для начала результаты замеров работы в x64:
Для 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


Кому интересно здесь объяснение как работает http://stackoverflow.com/questions/1583050/performance-surprise-with-as-and-nullable-types/3076525#3076525

В случае 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;

00007FFF75DC0A30  sub         rsp,28h  
00007FFF75DC0A34  xor         eax,eax
00007FFF75DC0A36  mov         qword ptr [rsp+20h],rax

00007FFF75DC0A3B  mov         rdx,rcx
00007FFF75DC0A3E  mov         rcx,7FF81883F918h
00007FFF75DC0A48  call        00007FFF75DC034C        ; CORINFO_HELP_ISINSTANCEOFANY
00007FFF75DC0A4D  mov         r8,rax
00007FFF75DC0A50  lea         rcx,[rsp+20h]
00007FFF75DC0A55  mov         rdx,7FF81883F918h
00007FFF75DC0A5F  call        00007FFF75DC0890        ; CORINFO_HELP_UNBOX_NULLABLE

;                 v.HasValue
00007FFF75DC0A64  movzx       eax,byte ptr [rsp+20h]
00007FFF75DC0A69  test        al,al
00007FFF75DC0A6B  je          00007FFF75DC0A76

;                 v.GetValueOrDefault()
00007FFF75DC0A6D  mov         eax,dword ptr [rsp+24h]
00007FFF75DC0A71  add         rsp,28h
00007FFF75DC0A75  ret

;                 Возвращаем 0
00007FFF75DC0A76  xor         eax,eax
00007FFF75DC0A78  add         rsp,28h
00007FFF75DC0A7C  ret

... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re: Минутка WTF-12: из реального проекта
От: rameel https://github.com/rsdn/CodeJam
Дата: 06.11.16 14:40
Оценка: 8 (1)
Здравствуйте, rameel, Вы писали:

В догонку, я тут еще обнаружил, что для pattern matching в C#7 используется проверка через приведение к Nullable.

Вот из этого
public int GetNumber(object value)
{
    if (value is int n)
        return n;
    
    return 0;
}

генерируется вот это (Пример с if (TryRoslyn)):
public int GetNumber(object value)
{
    int? num = value as int?;
    int valueOrDefault = num.GetValueOrDefault();
    if (num.HasValue)
    {
            return valueOrDefault;
    }
    return 0;
}

Это справедливо и для switch с расширинным case (Пример со switch (TryRoslyn)).
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[3]: Минутка WTF-12: из реального проекта
От: rameel https://github.com/rsdn/CodeJam
Дата: 06.11.16 14:45
Оценка: 5 (1)
Здравствуйте, Sharov, Вы писали:

S>Мне кажется здесь грабель

S> if (v != null)

Нет, не в этом. C# в случае с nullable преобразует такой код в if (v.HasValue). Это безобидный кусок кода
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re[2]: Минутка WTF-12: из реального проекта
От: rameel https://github.com/rsdn/CodeJam
Дата: 06.11.16 14:45
Оценка: 10 (1)
Здравствуйте, Codechanger, Вы писали:

C>Я правильно понимаю, что грабли зарыты в value as int?; ?


Да, именно.

C>Судя по всему там анбокс до инта, потом создание nullable<int>, потом приведение к нему.


Там все несколько сложнее. Объяснил ответом на стартовое сообщение: Re: Минутка WTF-12: из реального проекта
Автор: rameel
Дата: 06.11.16
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
Re: Минутка WTF-12: из реального проекта
От: Sinix  
Дата: 07.01.17 16:42
Оценка:
Здравствуйте, rameel, Вы писали:

R>В дополнение к серии на тему различных WTF, начатаю ув. Sinix


А пометь плиз тегом "Минутка WTF"
Re[2]: Минутка WTF-12: из реального проекта
От: rameel https://github.com/rsdn/CodeJam
Дата: 08.01.17 13:50
Оценка: 24 (1)
Здравствуйте, Sinix, Вы писали:

S>А пометь плиз тегом "Минутка WTF"


Пометил
... << RSDN@Home 1.0.0 alpha 5 rev. 0>>
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.