Удивление производительности с “как” и nullable типы

CPAN к спасению: IO:: LockedFile.

326
задан Jon Seigel 6 April 2010 в 15:46
поделиться

8 ответов

Очевидно, что машинный код, который JIT-компилятор может сгенерировать для первого случая, намного более эффективен. Одно правило, которое действительно помогает, заключается в том, что объект можно распаковать только в переменную, имеющую тот же тип, что и значение в коробке. Это позволяет JIT-компилятору генерировать очень эффективный код без необходимости преобразования значений.

Проверка оператора is проста, просто проверьте, не является ли объект нулевым и имеет ли объект ожидаемый тип, требует лишь нескольких инструкций машинного кода. Приведение также простое, JIT-компилятор знает расположение битов значения в объекте и использует их напрямую. Никакого копирования или преобразования не происходит, весь машинный код является встроенным и требует около дюжины инструкций. Это должно было быть действительно эффективным в .NET 1.0, когда бокс был обычным явлением.

Преобразование в int? требует намного больше работы. Представление значения упакованного целого числа несовместимо с разметкой памяти Nullable . Требуется преобразование, а код сложен из-за возможных типов перечислений в штучной упаковке. Компилятор JIT генерирует вызов вспомогательной функции CLR с именем JIT_Unbox_Nullable, чтобы выполнить задание. Это функция общего назначения для любого типа значения, там много кода для проверки типов. И значение копируется. Трудно оценить стоимость, поскольку этот код заперт внутри mscorwks.dll, но, вероятно, сотни инструкций машинного кода.

Метод расширения Linq OfType () также использует оператор is и приведение типов. Однако это приведение к универсальному типу.Компилятор JIT генерирует вызов вспомогательной функции JIT_Unbox (), которая может выполнять приведение к произвольному типу значения. У меня нет четкого объяснения, почему это так медленно, как приведение к Nullable , учитывая, что требуется меньше работы. Я подозреваю, что здесь ngen.exe может вызвать проблемы.

206
ответ дан 23 November 2019 в 00:51
поделиться

Интересно, что я передал отзыв о поддержке операторов через dynamic , который на порядок медленнее для Nullable (аналогично этот ранний тест ) - подозреваю по очень похожим причинам.

Должен любить Nullable . Еще один интересный момент заключается в том, что даже несмотря на то, что JIT выявляет (и удаляет) null для структур, не допускающих значения NULL, он исключает его для Nullable :

using System;
using System.Diagnostics;
static class Program {
    static void Main() { 
        // JIT
        TestUnrestricted<int>(1,5);
        TestUnrestricted<string>("abc",5);
        TestUnrestricted<int?>(1,5);
        TestNullable<int>(1, 5);

        const int LOOP = 100000000;
        Console.WriteLine(TestUnrestricted<int>(1, LOOP));
        Console.WriteLine(TestUnrestricted<string>("abc", LOOP));
        Console.WriteLine(TestUnrestricted<int?>(1, LOOP));
        Console.WriteLine(TestNullable<int>(1, LOOP));

    }
    static long TestUnrestricted<T>(T x, int loop) {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
    static long TestNullable<T>(T? x, int loop) where T : struct {
        Stopwatch watch = Stopwatch.StartNew();
        int count = 0;
        for (int i = 0; i < loop; i++) {
            if (x != null) count++;
        }
        watch.Stop();
        return watch.ElapsedMilliseconds;
    }
}
19
ответ дан 23 November 2019 в 00:51
поделиться

У меня нет времени попробовать, но вы можете иметь:

foreach (object o in values)
        {
            int? x = o as int?;

as

int? x;
foreach (object o in values)
        {
            x = o as int?;

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

8
ответ дан 23 November 2019 в 00:51
поделиться

Мне кажется, что isinst очень медленно работает с типами, допускающими значение NULL. В методе FindSumWithCast я изменил

if (o is int)

на

if (o is int?)

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

isinst     [mscorlib]System.Int32

заменяется на

isinst     valuetype [mscorlib]System.Nullable`1<int32>
26
ответ дан 23 November 2019 в 00:51
поделиться
using System;
using System.Diagnostics;
using System.Linq;

class Test
{
    const int Size = 30000000;

    static void Main()
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithCast(values);
        FindSumWithAsAndHas(values);
        FindSumWithAsAndIs(values);


        FindSumWithIsThenAs(values);
        FindSumWithIsThenConvert(values);

        FindSumWithLinq(values);



        Console.ReadLine();
    }

    static void FindSumWithCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int)o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Cast: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsAndHas(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Has: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }


    static void FindSumWithAsAndIs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;
            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As and Is: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }







    static void FindSumWithIsThenAs(object[] values)
    {
        // Apple-to-apple comparison with Cast routine above.
        // Using the similar steps in Cast routine above,
        // the AS here cannot be slower than Linq.



        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {

            if (o is int)
            {
                int? x = o as int?;
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then As: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithIsThenConvert(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {            
            if (o is int)
            {
                int x = Convert.ToInt32(o);
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Convert: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }



    static void FindSumWithLinq(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = values.OfType<int>().Sum();
        sw.Stop();
        Console.WriteLine("LINQ: {0} : {1}", sum,
                          (long)sw.ElapsedMilliseconds);
    }
}

Выводы:

Cast: 10000000 : 456
As and Has: 10000000 : 2103
As and Is: 10000000 : 2029
Is then As: 10000000 : 1376
Is then Convert: 10000000 : 566
LINQ: 10000000 : 1811

[РЕДАКТИРОВАТЬ: 2010-06-19]

Примечание. Предыдущий тест проводился в VS, отладка конфигурации с использованием VS2009, с использованием Core i7 (машина для разработки компании).

Следующее было сделано на моей машине с использованием Core 2 Duo, с использованием VS2010

Inside VS, Configuration: Debug

Cast: 10000000 : 309
As and Has: 10000000 : 3322
As and Is: 10000000 : 3249
Is then As: 10000000 : 1926
Is then Convert: 10000000 : 410
LINQ: 10000000 : 2018




Outside VS, Configuration: Debug

Cast: 10000000 : 303
As and Has: 10000000 : 3314
As and Is: 10000000 : 3230
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 418
LINQ: 10000000 : 1944




Inside VS, Configuration: Release

Cast: 10000000 : 305
As and Has: 10000000 : 3327
As and Is: 10000000 : 3265
Is then As: 10000000 : 1942
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1932




Outside VS, Configuration: Release

Cast: 10000000 : 301
As and Has: 10000000 : 3274
As and Is: 10000000 : 3240
Is then As: 10000000 : 1904
Is then Convert: 10000000 : 414
LINQ: 10000000 : 1936
7
ответ дан 23 November 2019 в 00:51
поделиться

Я попробовал точную конструкцию проверки типа

typeof (int) == item.GetType () , которая работает так же быстро, как элемент имеет версию int , и всегда возвращает число (выделение: даже если вы записали в массив Nullable , вам нужно будет использовать typeof (int) ). Вам также потребуется дополнительная проверка null! = Item .

Однако

typeof (int?) == item.GetType () остается быстрым (в отличие от item is int? ), но всегда возвращает false.

Конструкция typeof, на мой взгляд, является самым быстрым способом точной проверки типов, поскольку она использует RuntimeTypeHandle. Поскольку точные типы в этом случае не совпадают с типом, допускающим значение NULL, я предполагаю, что is / as должны сделать здесь дополнительный тяжелый лифтинг, чтобы убедиться, что это на самом деле экземпляр типа Nullable.

И честно говоря: что дает вам Nullable плюс HasValue ? Ничего такого. Вы всегда можете перейти непосредственно к базовому (значению) типу (в этом случае). Вы либо получаете значение, либо «нет, не экземпляр того типа, который вы просили». Даже если вы записали в массив (int?) Null , проверка типа вернет false.

8
ответ дан 23 November 2019 в 00:51
поделиться

Это результат FindSumWithAsAndHas выше: alt text

Это результат FindSumWithCast: alt text

Выводы:

  • Используя как , сначала проверяется, является ли объект экземпляром Int32; под капотом он использует isinst Int32 (который похож на рукописный код: if (o is int)). И используя как , он также безоговорочно распаковывает объект. И это настоящий убийца производительности - вызвать свойство (это все еще функция под капотом), IL_0027

  • Используя приведение, вы сначала проверяете, является ли объект int if (o is int) ; под капотом используется isinst Int32 . Если это экземпляр int, то вы можете безопасно распаковать значение, IL_002D

Проще говоря, это псевдокод использования подхода как :

int? x;

(x.HasValue, x.Value) = (o isinst Int32, o unbox Int32)

if (x.HasValue)
    sum += x.Value;    

И это псевдокод использования подхода с приведением:

if (o isinst Int32)
    sum += (o unbox Int32)

Итак, приведение ( (int) a [i] , синтаксис выглядит как приведение, но на самом деле распаковка, приведение и распаковка используют один и тот же синтаксис, в следующий раз я Буду педантичен с правильной терминологией) подход действительно быстрее, вам нужно только распаковать значение, когда объект явно является int . Нельзя сказать то же самое об использовании подхода как .

12
ответ дан 23 November 2019 в 00:51
поделиться

Профилирование дальше:

using System;
using System.Diagnostics;

class Program
{
    const int Size = 30000000;

    static void Main(string[] args)
    {
        object[] values = new object[Size];
        for (int i = 0; i < Size - 2; i += 3)
        {
            values[i] = null;
            values[i + 1] = "";
            values[i + 2] = 1;
        }

        FindSumWithIsThenCast(values);

        FindSumWithAsThenHasThenValue(values);
        FindSumWithAsThenHasThenCast(values);

        FindSumWithManualAs(values);
        FindSumWithAsThenManualHasThenValue(values);



        Console.ReadLine();
    }

    static void FindSumWithIsThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            if (o is int)
            {
                int x = (int)o;
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Is then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenHasThenCast(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (x.HasValue)
            {
                sum += (int)o;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Has then Cast: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithManualAs(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            bool hasValue = o is int;
            int x = hasValue ? (int)o : 0;

            if (hasValue)
            {
                sum += x;
            }
        }
        sw.Stop();
        Console.WriteLine("Manual As: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

    static void FindSumWithAsThenManualHasThenValue(object[] values)
    {
        Stopwatch sw = Stopwatch.StartNew();
        int sum = 0;
        foreach (object o in values)
        {
            int? x = o as int?;

            if (o is int)
            {
                sum += x.Value;
            }
        }
        sw.Stop();
        Console.WriteLine("As then Manual Has then Value: {0} : {1}", sum,
                            (long)sw.ElapsedMilliseconds);
    }

}

Выход:

Is then Cast: 10000000 : 303
As then Has then Value: 10000000 : 3524
As then Has then Cast: 10000000 : 3272
Manual As: 10000000 : 395
As then Manual Has then Value: 10000000 : 3282

Что мы можем заключить из этих цифр?

  • Во-первых, подход is-then-cast значительно быстрее, чем подход as. 303 против 3524
  • Во-вторых, .Value незначительно медленнее, чем casting. 3524 vs 3272
  • В-третьих, .HasValue незначительно медленнее, чем использование ручного has (т.е. использование is). 3524 vs 3282
  • В-четвертых, сравнивая simulated as и real as, мы видим, что simulated as все еще значительно быстрее, чем real as. 395 против 3524
  • Наконец, исходя из первого и четвертого выводов, что-то не так с as implementation ^_^
9
ответ дан 23 November 2019 в 00:51
поделиться
Другие вопросы по тегам:

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