Производительность удивляет с помощью типов “as” и nullable

StackOverflow https://stackoverflow.com/questions/1583050

Вопрос

Я просто подробно пересматриваю главу 4 C #, в которой рассматриваются типы с нулевым значением, и добавляю раздел об использовании оператора "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".

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

Результаты:

Бросок:10000000 :121
Как: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);
    }
}
Это было полезно?

Решение

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

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

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

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

Другие советы

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

if (o is int)

Для

if (o is int?)

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

isinst     [mscorlib]System.Int32

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

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

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

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

Вот что isinst делает (ECMA 335 Раздел III, 4.6):

Формат: исинст типеТок

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

Если типеТок является типом значения, не допускающим обнуления, или универсальным типом параметра, который интерпретируется как “упакованный”. типеТок.

Если типеТок является обнуляемым типом, Nullable<T>, это интерпретируется как “упакованный в коробку”. T

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

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

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

Почему он это делает? isinst T? никогда не имеет эффекта, который был бы очевиден, т.е.ты получаешь обратно 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 типеТок.

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

Обычно, unbox просто вычисляет адрес типа значения, который уже присутствует внутри упакованного объекта.Такой подход невозможен при распаковке типов значений с нулевым значением.Потому что Nullable<T> значения преобразуются в упакованные 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 text

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

Выводы:

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

  • Используя приведение, вы сначала проверяете, является ли объект int if (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
  • Во-вторых, .Value выполняется незначительно медленнее, чем приведение.3524 против 3272
  • В-третьих, .HasValue работает незначительно медленнее, чем при использовании ручного has (т.е.используя является).3524 против 3282
  • В-четвертых, сравнение яблока с яблоком (т.е.как присвоение моделируемого значения HasValue, так и преобразование моделируемого значения происходят вместе) между моделируется как и реальный , как приближаемся, мы можем видеть моделируется как все еще значительно быстрее, чем реальный , как.395 против 3524
  • Наконец, основываясь на первом и четвертом выводах, есть что-то неправильное в как реализация ^_^

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

Оригинальный пример операционной системы...

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) переопределение (унаследованное от Object) к нему следующим образом:

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

 


Приложение: В 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').

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

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, на мой взгляд, является самым быстрым способом для точный проверка типа, поскольку она использует RuntimeTypeHandle.Поскольку точные типы в этом случае не совпадают с nullable, я предполагаю, что, is/as здесь нужно приложить дополнительные усилия, чтобы убедиться, что это действительно экземпляр типа с нулевым значением.

И честно:что делает ваш 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

[ПРАВИТЬ: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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top