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

330

Я просто пересматриваю четвертую главу 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);
    }
}
Джон Скит
источник
8
Почему бы не посмотреть на приведенный код? Даже VS отладчик может показать это.
Антон Тихий
2
Мне просто любопытно, вы также тестировали с CLR 4.0?
Дирк Воллмар
1
@Антон: Хороший вопрос. Подойдет в какой-то момент (хотя в данный момент этого нет в VS :) @divo: Да, и все вокруг хуже. Но тогда это в бета-версии, так что там может быть много кода отладки.
Джон Скит
1
Сегодня я узнал, что вы можете использовать asна обнуляемых типов. Интересно, так как его нельзя использовать для других типов значений. На самом деле, более удивительно.
Леппи
3
@ Лепп имеет смысл, чтобы он не работал с типами значений. Подумайте об этом, asпопытайтесь привести к типу, и если он потерпит неудачу, он вернет ноль. Вы не можете установить типы значений в null
Earlz

Ответы:

209

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

Ганс Пассант
источник
16
Хорошо, я убежден. Я думаю, что я привык думать о «есть» как о потенциально дорогой из-за возможности подняться по иерархии наследования - но в случае типа значения нет возможности иерархии, так что это может быть простое побитовое сравнение , Я все еще думаю, что JIT-код для случая, допускающего обнуляемость, может быть оптимизирован JIT гораздо более интенсивно, чем сейчас.
Джон Скит
26

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

if (o is int)

в

if (o is int?)

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

isinst     [mscorlib]System.Int32

меняется на

isinst     valuetype [mscorlib]System.Nullable`1<int32>
Дирк Воллмар
источник
1
Это больше, чем это; в случае «приведения» isinstследует проверка на ничтожность, а затем условно an unbox.any. В обнуляемом случае есть безусловное unbox.any .
Джон Скит
Да, получается оба isinst и unbox.anyмедленнее на обнуляемых типах.
Дирк Воллмар
@Jon: Вы можете просмотреть мой ответ о том, зачем нужен актерский состав. (Я знаю, что это старый, но я только что обнаружил это q и подумал, что должен предоставить свой 2c того, что я знаю о CLR).
Йоханнес Рудольф
22

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

Во-первых, 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>в куче и вычислять адрес для вновь выделенного объекта.

Йоханнес Рудольф
источник
Я думаю, что в самом последнем цитируемом предложении может быть опечатка; не должно ли «... в куче ...» быть «в стеке выполнения ?» Похоже, распаковка обратно в какой-то новый экземпляр кучи GC заменяет исходную проблему на почти идентичную новую.
Гленн
19

Интересно, что я передал отзыв о поддержке оператора, так как 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;
    }
}
Марк Гравелл
источник
Yowser. Это действительно болезненная разница. Ик.
Джон Скит
Если из всего этого не вышло ничего хорошего, это заставило меня включить предупреждения как для моего исходного кода, так и для этого :)
Джон Скит
Я знаю, что это старый вопрос, но не могли бы вы объяснить, что вы подразумеваете под «точками JIT (и удаляет) nullдля не допускающих обнуление структур»? Вы имеете в виду, что он заменяет nullзначение по умолчанию или что-то во время выполнения?
Джастин Морган
2
@Justin - универсальный метод может использоваться во время выполнения с любым количеством перестановок универсальных параметров (и Tт. Д.). Требования к стеку и т. Д. Зависят от аргументов (количество стекового пространства для локального и т. Д.), Поэтому вы получаете один JIT для любой уникальной перестановки, включающей тип значения. Тем не менее, ссылки имеют одинаковый размер, поэтому используйте JIT. Выполняя JIT для каждого значения, он может проверить несколько очевидных сценариев и попытаться вырезать недоступный код из-за таких вещей, как невозможные нули. Это не идеально, заметьте. Кроме того, я игнорирую AOT для вышеупомянутого.
Марк Гравелл
Неограниченный обнуляемый тест все еще на 2,5 порядка медленнее, но происходит некоторая оптимизация, когда вы не используете countпеременную. Добавление Console.Write(count.ToString()+" ");после watch.Stop();в обоих случаях замедляет другие тесты почти на порядок, но неограниченный обнуляемый тест не изменяется. Обратите внимание, что есть также изменения при тестировании случаев, когда nullон пройден, подтверждая, что исходный код на самом деле не выполняет нулевую проверку и приращение для других тестов. Linqpad
Марк Херд
12

Это результат 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подход.

Майкл Буэн
источник
11

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

Гленн Слэйден
источник
9

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

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 незначительно медленнее , чем руководство (т.е. использование является ). 3524 против 3282
  • В-четвертых, выполняя сравнение между яблоками (т. Е. Как присвоение имитированного значения HasValue, так и преобразование имитируемого значения происходит вместе) между имитированным как и реальным как подходом, мы можем увидеть, что симуляция все еще значительно быстрее, чем реальная, поскольку . 395 против 3524
  • И, наконец, на основе первого и четвертого заключение отметим , что что - то не так с в реализации ^ _ ^
Майкл Буэн
источник
8

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

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

так как

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

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

Джеймс Блэк
источник
1
Нет, я запустил это, и это немного медленнее.
Хенк Холтерман
2
Объявление переменной в другом месте значительно влияет на сгенерированный код только тогда, когда переменная захвачена (в этот момент это влияет на фактическую семантику) в моем опыте. Обратите внимание, что он не создает новый объект в куче, хотя, безусловно, создает новый экземпляр int?в стеке с помощью unbox.any. Я подозреваю, что это проблема - я предполагаю, что созданный вручную IL может побить оба варианта здесь ... хотя также возможно, что JIT оптимизирован для распознавания для случая is / cast и проверяется только один раз.
Джон Скит
Я думал, что актерский состав, вероятно, оптимизирован, так как он был вокруг так долго.
Джеймс Блэк
1
is / cast - легкая цель для оптимизации, это такая досадно распространенная идиома.
Антон Тихий
4
Локальные переменные распределяются в стеке при создании фрейма стека для метода, поэтому в случае объявления переменной в методе нет никакой разницы. (Если, конечно, это не закрытие, но здесь дело не в этом.)
Гуффа
8

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

typeof(int) == item.GetType(), которая работает так же быстро, как и item is intверсия, и всегда возвращает число (выделение: даже если вы записали a 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.

DALO
источник
Интересно ... идея использования "as" + HasValue (not is plus HasValue, обратите внимание) состоит в том, что он выполняет проверку типа только один раз вместо двух. Это делает «проверить и распаковать» в один шаг. Такое ощущение, что это должно быть быстрее ... но это явно не так. Я не уверен, что вы подразумеваете под последним предложением, но в штучной упаковке не существует такой вещи int?- если вы укажете int?значение, оно в конечном итоге будет заключено в рамку как int или nullссылка.
Джон Скит
7
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
Майкл Буэн
источник
Какую версию фреймворка вы используете, из интереса? Результаты на моем нетбуке (с использованием .NET 4RC) еще более впечатляющие - версии, использующие As, намного хуже, чем ваши результаты. Может быть, они улучшили его для .NET 4 RTM? Я все еще думаю, что это может быть быстрее ...
Джон Скит
@Michael: Вы запускали неоптимизированную сборку или работали в отладчике?
Джон Скит
@Jon: неоптимизированная сборка, под отладчиком
Майкл Буэн
1
@Michael: Верно - я склонен рассматривать результаты производительности в отладчике как не имеющие отношения к делу :)
Джон Скит
@Jon: Если под отладчиком, то есть внутри VS; да, предыдущий тест был сделан под отладчиком. Я снова прошёл бенчмаркинг внутри VS и вне его, скомпилировал как отладочный и скомпилировал как релиз. Проверьте редактирование
Майкл Буэн