Сюрприз производительности с типами " as " и nullable


Я просто пересматриваю Главу 4 C#, в которой подробно рассматриваются типы nullable, и я добавляю раздел об использовании оператора "as", который позволяет вам писать:

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    ... // Use x.Value in here
}

Я думал, что это было действительно аккуратно, и что это может улучшить производительность по сравнению с эквивалентом C# 1, используя "is" с последующим приведением - в конце концов, таким образом, нам нужно только попросить динамическую проверку типа один раз, а затем простую проверку значения.

это, кажется, не так, однако. Я ниже приведен пример тестового приложения, который в основном суммирует все целые числа в массиве объектов, но массив содержит много пустых ссылок и ссылок на строки, а также целые числа в коробках. Тест измеряет код, который вам нужно будет использовать в C# 1, код, использующий оператор "as", и просто для пинки решение LINQ. К моему удивлению, код C# 1 в этом случае в 20 раз быстрее - и даже код LINQ (который, как я ожидал, будет медленнее, учитывая задействованные итераторы) бьет " as" код.

является реализация .NET isinst для nullable типов просто очень медленно? Это дополнительный unbox.any что вызывает проблему? Есть ли другое объяснение этому? На данный момент кажется, что мне придется включить предупреждение против использования этого в ситуациях, чувствительных к производительности...

результаты:

Cast: 10000000: 121
As: 10000000: 2211
LINQ: 10000000: 2143

код:

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);
        FindSumWithAs(values);
        FindSumWithLinq(values);
    }

    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 FindSumWithAs(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: {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);
    }
}
10 302

10 ответов:

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

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

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

мне кажется, что isinst просто очень медленно на nullable типов. В методе FindSumWithCast сменил

if (o is int)

до

if (o is int?)

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

isinst     [mscorlib]System.Int32

меняется на

isinst     valuetype [mscorlib]System.Nullable`1<int32>

это первоначально началось как комментарий к превосходному ответу Ханса Пассанта, но он стал слишком длинным, поэтому я хочу добавить несколько битов здесь:

во-первых, C# as оператор выдает isinst Il инструкция (так же как и is оператор). (Еще одна интересная инструкция castclass, испускаемому, когда вы сделать прямой бросок, и компилятор знает, что проверка выполнения не может быть опущен.)

вот что isinst у (ECMA 335 раздел III, 4.6):

: isinsttypeTok

typeTok - это маркер метаданных (a typeref,typedef или typespec), указывая нужный класс.

если typeTok это необнуляемый тип значений или параметр универсального типа он интерпретируется как "коробочные" typeTok.

если typeTok является типом, поддерживающим значение null, Nullable<T>, это интерпретируется как "в штучной упаковке"T

самое главное:

если фактический тип (не тип отслеживаемого верификатора) obj и verifier-assignable-to тип typeTok тогда isinst успешно и obj (как результат) возвращается без изменений, в то время как проверка отслеживает его тип как typeTok. в отличие от принуждения (§1.6) и преобразования (§3.27), isinst никогда не изменяет фактический тип объекта и сохраняет идентичность объекта (см. Раздел I).

Итак, убийца производительности не isinst в данном случае, но дополнительный unbox.any. Это было неясно из ответа Ганса, поскольку он смотрел только на JITed-код. В общем случае компилятор C# будет выдавать unbox.any после a isinst T? (но опустит его в случае, если вы делаете isinst T, когда T является ссылочным типом).

почему он это делает? isinst T? никогда не имеет эффекта, что было бы очевидно, т. е. вы получите обратно a T?. Вместо этого все эти инструкции гарантируют, что у вас есть "boxed T" это можно распаковать в T?. Чтобы получить фактический T?, нам все еще нужно распаковать наш "boxed T" до T?, поэтому компилятор выдает unbox.any после isinst. Если вы думаете об этом, это имеет смысл, поскольку "формат" для T? это просто "boxed T" и что делает castclass и isinst выполнить распаковку было бы непоследовательно.

поддержка вверх по находке Ганса с некоторой информацией от стандартный здесь:

(ECMA 335 раздел III, 4.33):unbox.any

при применении к коробчатой форме типа значения unbox.any инструкция извлекает значение, содержащееся в obj (типа O). (Это эквивалентно unbox следовал по ldobj.) При применении к ссылочному типу,unbox.any инструкция имеет тот же эффект, что и castclass typeTok.

(ECMA 335 раздел III, 4.32):unbox

как правило, unbox просто вычисляет адрес типа значения, который уже присутствует внутри упакованного объекта. Такой подход невозможен при распаковке типов значений с нулевым значением. Потому что Nullable<T> значения преобразуются в boxed Ts во время работы коробки реализация часто должна производить новый Nullable<T> в куче и вычислить адрес для вновь выделенного объект.

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

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

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;
    }
}

это результат FindSumWithAsAndHas выше: alt текст http://www.freeimagehosting.net/uploads/9e3c0bfb75.png

это результат FindSumWithCast: alt текст http://www.freeimagehosting.net/uploads/ce8a5a3934.png

выводы:

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

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

проще говоря, это псевдо-код, используя as подход:

int? x;

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

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

и это псевдо-код использования cast подхода:

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

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

далее профилирования:

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 подход значительно быстрее, чем как подход. 303 против 3524
  • во-вторых, .Значение немного медленнее, чем литье. 3524 против 3272
  • в-третьих, .HasValue немного медленнее, чем при использовании руководства (т. е. с помощью и). 3524 против 3282
  • в-четвертых, делать устройствами Apple сравнение(т. е. назначение условного свойство hasvalue и преобразование имитируемых значений происходит вместе) между моделируется и реально, как подходим, видно моделируется по-прежнему значительно быстрее, чем реально, как. 395 против 3524
  • наконец, основываясь на первом и четвертом выводе, что-то не так с как реализация ^_^

у меня нет времени, чтобы попробовать его, но вы можете хотеть иметь:

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

как

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

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

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

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

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

typeof-construct в моих глазах самый быстрый способ для точно проверка типа, так как он использует RuntimeTypeHandle. Поскольку точные типы в этом случае не совпадают с nullable, я предполагаю,is/as здесь нужно сделать дополнительный heavylifting, чтобы убедиться, что это на самом деле экземпляр типа Nullable.

и честно: что делает ваш is Nullable<xxx> plus HasValue купить можно? Ничего. Вы всегда можете перейти непосредственно к базовому типу (value) (в этом случае). Вы либо получаете значение, либо "нет, не экземпляр типа, который вы просили". Даже если вы написали (int?)null для массива проверка типа вернет false.

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

[EDIT: 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

чтобы сохранить этот ответ в актуальном состоянии, стоит отметить, что большая часть обсуждения на этой странице теперь спорна с C# 7.1 и .NET 4.7 который поддерживает тонкий синтаксис, который также создает лучший код IL.

исходный пример OP...

object o = ...;
int? x = o as int?;
if (x.HasValue)
{
    // ...use x.Value in here
}

становится просто...

if (o is int x)
{
    // ...use x in here
}

я обнаружил, что одно общее использование для нового синтаксиса - это когда вы пишете .NET тип значения (т. е. struct на C#), что реализует IEquatable<MyStruct> (как большинство). После реализации строго типизированного Equals(MyStruct other) метод, теперь вы можете изящно перенаправить нетипизированный Equals(Object obj) override (наследуется от Object) к нему следующим образом:

public override bool Equals(Object obj) => obj is MyStruct o && Equals(o);


приложение: The Release построить IL здесь приведен код для первых двух примеров функций, показанных выше в этом ответе (соответственно). В то время как код IL для новый синтаксис действительно на 1 байт меньше, он в основном выигрывает большой, делая нулевые вызовы (против двух) и избегая unbox операции в целом, когда это возможно.

// static void test1(Object o, ref int y)
// {
//     int? x = o as int?;
//     if (x.HasValue)
//         y = x.Value;
// }

[0] valuetype [mscorlib]Nullable`1<int32> x
        ldarg.0
        isinst [mscorlib]Nullable`1<int32>
        unbox.any [mscorlib]Nullable`1<int32>
        stloc.0
        ldloca.s x
        call instance bool [mscorlib]Nullable`1<int32>::get_HasValue()
        brfalse.s L_001e
        ldarg.1
        ldloca.s x
        call instance !0 [mscorlib]Nullable`1<int32>::get_Value()
        stind.i4
L_001e: ret

// static void test2(Object o, ref int y)
// {
//     if (o is int x)
//         y = x;
// }

[0] int32 x,
[1] object obj2
        ldarg.0
        stloc.1
        ldloc.1
        isinst int32
        ldnull
        cgt.un
        dup
        brtrue.s L_0011
        ldc.i4.0
        br.s L_0017
L_0011: ldloc.1
        unbox.any int32
L_0017: stloc.0
        brfalse.s L_001d
        ldarg.1
        ldloc.0
        stind.i4
L_001d: ret

для дальнейшего тестирования, которое подтверждает мое замечание о производительности нового C#7 синтаксис, превосходящий ранее доступные параметры, см. здесь (в частности, пример 'D').