Производительность сюрприз с «как» и обнуляемые типы

Я просто пересматриваю четвертую главу C # in Depth, посвященную обнуляемым типам, и добавляю раздел об использовании оператора «as», который позволяет писать:

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

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

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

Является ли реализация .NET 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);
    }
}
17.10.2009 19:48:57
Почему бы не посмотреть на приведенный код? Даже VS отладчик может показать это.
Anton Tykhyy 17.10.2009 20:28:00
Мне просто любопытно, вы также тестировали с CLR 4.0?
Dirk Vollmar 17.10.2009 20:32:37
@Антон: Хороший вопрос. Подойдет в какой-то момент (хотя в данный момент этого нет в VS :) @divo: Да, и все вокруг хуже. Но тогда это в бета-версии, поэтому там может быть много кода отладки.
Jon Skeet 17.10.2009 20:37:14
Сегодня я узнал, что вы можете использовать asна обнуляемых типов. Интересно, так как его нельзя использовать для других типов значений. На самом деле, более удивительно.
leppie 19.06.2010 10:20:06
@ Лепп имеет смысл, чтобы он не работал с типами значений. Подумайте об этом, asпопытайтесь привести к типу, и если он потерпит неудачу, он вернет ноль. Вы не можете установить типы значений в null
Earlz 19.06.2010 18:53:58
10 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

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

207
20.06.2010 12:06:33
Хорошо, я убежден. Я предполагаю, что привык думать о «есть» как о потенциально дорогостоящем из-за возможности подняться по иерархии наследования - но в случае типа значения нет возможности иерархии, поэтому это может быть простое побитовое сравнение , Я все еще думаю, что JIT-код для случая, допускающего обнуляемость, мог бы быть оптимизирован JIT гораздо сильнее, чем сейчас.
Jon Skeet 19.06.2010 18:21:28

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

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

в виде

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

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

8
17.10.2009 19:59:51
Нет, я запустил это, и это немного медленнее.
Henk Holterman 17.10.2009 20:03:38
Объявление переменной в другом месте значительно влияет на сгенерированный код, только когда переменная захвачена (в этот момент она влияет на фактическую семантику) в моем опыте. Обратите внимание, что он не создает новый объект в куче, хотя он, безусловно, создает новый экземпляр int?в стеке с помощью unbox.any. Я подозреваю, что это проблема - я предполагаю, что созданный вручную IL может побить оба варианта здесь ... хотя также возможно, что JIT оптимизирован для распознавания для случая is / cast и проверяется только один раз.
Jon Skeet 17.10.2009 20:08:14
Я думал, что актерский состав, вероятно, оптимизирован, так как он был вокруг так долго.
James Black 17.10.2009 20:17:46
is / cast - легкая цель для оптимизации, это такая досадно распространенная идиома.
Anton Tykhyy 17.10.2009 20:26:19
Локальные переменные размещаются в стеке при создании фрейма стека для метода, поэтому в случае объявления переменной в методе нет никакой разницы. (Если, конечно, это не закрытие, но здесь дело не в этом.)
Guffa 17.10.2009 21:30:54

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

if (o is int)

в

if (o is int?)

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

isinst     [mscorlib]System.Int32

меняется на

isinst     valuetype [mscorlib]System.Nullable`1<int32>
26
17.10.2009 20:10:02
Это больше, чем это; в случае «приведения» isinstследует проверка на ничтожность и затем условно an unbox.any. В обнуляемом случае есть безусловное unbox.any .
Jon Skeet 17.10.2009 20:14:36
Да, получается оба isinst и unbox.anyмедленнее на обнуляемых типах.
Dirk Vollmar 17.10.2009 20:26:54
@Jon: Вы можете просмотреть мой ответ о том, зачем нужен актерский состав. (Я знаю, что это старый, но я только что обнаружил это q и подумал, что должен предоставить свой 2c из того, что я знаю о CLR).
Johannes Rudolph 17.08.2011 20:24:57

Интересно, что я передал отзыв о поддержке оператора, так как 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;
    }
}
19
17.10.2009 21:26:50
Yowser. Это действительно болезненная разница. Ик.
Jon Skeet 17.10.2009 21:32:41
Если из всего этого не вышло ничего хорошего, это заставило меня включить предупреждения как для моего исходного кода, так и для этого :)
Jon Skeet 18.10.2009 11:52:26
Я знаю, что это старый вопрос, но не могли бы вы объяснить, что вы подразумеваете под «точками JIT (и удаляет) nullдля не допускающих обнуление структур»? Вы имеете в виду, что он заменяет nullзначение по умолчанию или что-то во время выполнения?
Justin Morgan 21.04.2011 21:23:41
@Justin - универсальный метод может использоваться во время выполнения с любым количеством перестановок универсальных параметров (и Tт. Д.). Требования к стеку и т. Д. Зависят от аргументов (количество стекового пространства для локального и т. Д.), Поэтому вы получаете один JIT для любой уникальной перестановки, включающей тип значения. Тем не менее, ссылки имеют одинаковый размер, поэтому используйте JIT. Выполняя JIT по типу значения, он может проверить несколько очевидных сценариев и попытаться вырезать недоступный код из-за таких вещей, как невозможные нули. Это не идеально, заметьте. Кроме того, я игнорирую AOT для вышеупомянутого.
Marc Gravell♦ 21.04.2011 21:32:24
Неограниченный обнуляемый тест все еще на 2,5 порядка медленнее, но происходит некоторая оптимизация, когда вы не используете countпеременную. Добавление Console.Write(count.ToString()+" ");после watch.Stop();в обоих случаях замедляет другие тесты почти на порядок, но неограниченный обнуляемый тест не изменяется. Обратите внимание, что есть также изменения при тестировании случаев, когда nullон пройден, подтверждая, что исходный код на самом деле не выполняет нулевую проверку и приращение для других тестов. Linqpad
Mark Hurd 20.05.2014 06:01:20
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
19.06.2010 10:16:59
Какую версию фреймворка вы используете, из интереса? Результаты на моем нетбуке (с использованием .NET 4RC) еще более впечатляющие - версии, использующие As, намного хуже, чем ваши результаты. Может быть, они улучшили его для .NET 4 RTM? Я все еще думаю, что это может быть быстрее ...
Jon Skeet 21.04.2010 06:06:29
@Michael: Вы запускали неоптимизированную сборку или работали в отладчике?
Jon Skeet 19.06.2010 09:10:54
@Jon: неоптимизированная сборка, под отладчиком
Michael Buen 19.06.2010 09:52:03
@Michael: Верно - я склонен рассматривать результаты производительности в отладчике как не имеющие отношения к делу :)
Jon Skeet 19.06.2010 09:53:06
@Jon: Если под отладчиком, то есть внутри VS; да, предыдущий тест был сделан под отладчиком. Я снова прошёл бенчмаркинг внутри VS и вне его, скомпилировал как отладочный и скомпилировал как релиз. Проверьте редактирование
Michael Buen 19.06.2010 10:19:17

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

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что здесь нужно выполнить дополнительное усиление , чтобы убедиться, что это на самом деле экземпляр типа Nullable.

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

8
19.06.2010 10:01:21
Интересно ... идея использования "as" + HasValue (not is plus + HasValue, обратите внимание) состоит в том, что он выполняет проверку типа только один раз вместо двух. Это делает "проверить и снять флажок" в один шаг. Такое ощущение, что это должно быть быстрее ... но это явно не так. Я не уверен, что вы подразумеваете под последним предложением, но в штучной упаковке такого понятия не существует int?- если вы укажете int?значение, оно будет заключено в рамку как int или nullссылка.
Jon Skeet 19.06.2010 10:17:15

Это результат FindSumWithAsAndHas выше: альтернативный текст

Это результат FindSumWithCast: альтернативный текст

Результаты:

  • Используя as, он сначала проверяет, является ли объект экземпляром Int32; под капотом он использует isinst Int32(что похоже на рукописный код: if (o is 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;    

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

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

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

12
30.07.2019 07:04:19

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

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

Что мы можем сделать из этих цифр?

  • Во- первых, это-то литой подход значительно быстрее , чем в подходе. 303 против 3524
  • Во-вторых, .Value незначительно медленнее, чем приведение. 3524 против 3272
  • В-третьих, .HasValue немного медленнее, чем при использовании вручную (то есть с использованием is ). 3524 против 3282
  • В-четвертых, выполняя сравнение между яблоками (т. Е. Как присвоение имитированного значения HasValue, так и преобразование имитируемого значения происходит вместе) между имитированным и реальным подходом, мы видим симуляцию, которая все еще значительно быстрее, чем реальная . 395 против 3524
  • И, наконец, на основе первого и четвертого заключение отметим , что что - то не так с в реализации ^ _ ^
9
21.06.2010 14:17:32

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

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

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

Формат: isinst typeTok

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

Если typeTok является ненулевым типом значения или универсальным типом параметра, он интерпретируется как «упакованный» typeTok .

Если typeTok является обнуляемым типом Nullable<T>, он интерпретируется как «в штучной упаковке»T

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

Если фактический тип (не верификатор отслеживается типа) OBJ является верификатором назначаемого к типу 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?никогда не имеет эффекта, который был бы очевиден, т.е. вы получаете обратно 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инструкция имеет тот же эффект, что и castclasstypeTok.

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

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

22
18.08.2017 17:48:19
Я думаю, что в самом последнем цитируемом предложении может быть опечатка; не должно ли «... в куче ...» быть «в стеке выполнения ?» Похоже, распаковка обратно в какой-то новый экземпляр кучи GC заменяет исходную проблему на почти идентичную новую.
Glenn Slayden 2.03.2019 05:38:20

Чтобы этот ответ был актуальным, стоит упомянуть, что большая часть обсуждения на этой странице теперь спорна с 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»).

10
28.03.2018 00:00:59