Несподіванка на продуктивність з типами "як" та зведеними на нуль


330

Я просто переглядаю розділ 4 C # в Depth, який стосується змінних типів, і я додаю розділ про використання оператора "as", який дозволяє писати:

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

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

Однак, мабуть, це не так. Нижче я включив зразок тестового додатка, який в основному підсумовує всі цілі числа в масиві об'єктів - але масив містить безліч нульових посилань і посилань рядків, а також цілих чисел у коробці. Базовий показник вимірює код, який вам доведеться використовувати в 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
@Anton: Добре. Зробить у якийсь момент (хоча наразі цього немає у VS :) @divo: Так, і це гірше все. Але тоді це в бета-версії, тому там може бути багато налагоджувального коду.
Джон Скіт

1
Сьогодні я дізнався, що ви можете використовувати asдля змінних типів. Цікаво, оскільки його не можна використовувати для інших типів цінності. Насправді, більш дивно.
леппі

3
@Lepp це має ідеальний сенс, щоб він не працював на типи значень. Подумайте про це, asнамагається передати тип і якщо це не вдасться, він повернеться до нуля. Ви не можете встановити значення "значення" на нуль
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?)

що також значно сповільнює виконання. Єдине, що я бачу, це ІЛ

isinst     [mscorlib]System.Int32

змінюється на

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

1
Це більше того; у випадку "литий" isinstпісля цього проводиться тест на недійсність, а потім умовно " an" unbox.any. У зворотному відмінку є безумовне unbox.any .
Джон Скіт

Так, виходить і те, isinst і unbox.anyповільніше на мінливих типах.
Дірк Волмар

@Jon: Ви можете переглянути мою відповідь на те, для чого потрібен акторський склад. (Я знаю, що це старе, але я щойно відкрив це питання і подумав, що я повинен надати 2с того, що я знаю про CLR).
Йоганнес Рудольф

22

Спочатку це почалося як коментар до відмінної відповіді Ганса Пассанта, але воно зайшло занадто довго, тому я хочу додати сюди кілька біт:

По-перше, asоператор C # видасть isinstінструкцію IL (так само isоператор). (Ще одна цікава інструкція, видається castclass, коли ви робите прямий склад, і компілятор знає, що перевірку виконання не можна вимкнути.)

Ось що isinstробить ( ECMA 335, розділ III, 4.6 ):

Формат: isinst typeTok

typeTok - це маркер метаданих (a typeref, typedefабо typespec), що вказує на потрібний клас.

Якщо typeTok - це значення, що не зводиться до нуля, або загальний тип параметра, він інтерпретується як " boxed " typeTok .

Якщо typeTok є нульовим типом Nullable<T>, він інтерпретується як "коробковий"T

Головне:

Якщо фактичний тип (не тип відстежуваного верифікатора) obj є verifier- присвоюваним-типу typeTok, тоді він isinstвдається, і obj (як результат ) повертається незмінним, коли верифікація відстежує його тип як typeTok . На відміну від примусів (§1.6) та перетворень (§3.27), isinstніколи не змінює фактичний тип об'єкта та зберігає ідентичність об'єкта (див. Розділ I).

Отже, вбивця продуктивності не isinstв цьому випадку, а додатковий unbox.any. Це не було зрозуміло з відповіді Ганса, оскільки він дивився лише на JITed-код. Взагалі компілятор C # випустить a 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виконання розпакування було б невідповідним.

Отож, підкріплюючи висновок Ганса з деякою інформацією зі стандарту , тут йдеться:

(Розділ III, 4.33 ECMA 335): unbox.any

При застосуванні до коробкової форми типу значень unbox.anyінструкція витягує значення, що міститься в obj (типу O). (Це еквівалентно unboxнаступному ldobj.) При застосуванні до посилального типу unbox.anyінструкція має той же ефект, що і castclasstypeTok.

(Розділ III, 4.32 ECMA 335): unbox

Зазвичай unboxпросто обчислюють адресу типу значення, який вже присутній всередині об'єкта, який знаходиться у вікні. Цей підхід неможливий при розпакуванні змінних типів значення. Оскільки Nullable<T>значення перетворюються Tsу вікно під час роботи з полем, реалізація часто повинна виготовляти нове Nullable<T>на купі та обчислювати адресу новопризначеному об'єкту.


Я думаю, що саме останнє речення, яке цитується, може мати помилку; не повинен "... на купі ..." бути "на стеці виконання ?" Схоже, розпакування назад у якийсь новий екземпляр купи GC замінює оригінальну проблему майже на однакову нову.
Гленн Слейден

19

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

Маю любов Nullable<T>. Ще однією nullцікавою є те, що навіть незважаючи на те, що JIT відзначає (і видаляє) для ненульових конструкцій, це вимагає для 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
Mark Hurd

12

Це результат FindSumWithAsAndHas вище: alt текст

Це результат FindSumWithCast: alt текст

Висновки:

  • Використовуючи 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;    

І це псевдокод використання підходу cast:

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

Тож акторський склад ( (int)a[i]ну синтаксис схожий на акторський склад, але він насправді розпаковує, кидає та unboxing ділиться тим самим синтаксисом; наступного разу я буду педантичним із правильною термінологією) підхід дійсно швидший, вам потрібно було лише розблокувати значення коли об'єкт, безумовно, є 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. Я підозрюю, що це питання - я здогадуюсь, що ІЛ-майстер може перемогти обидва варіанти тут ... хоча також можливо, що JIT оптимізовано для розпізнавання для є / 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? ), але завжди повертається хибним.

На моїх очах typeof-конструкція є найшвидшим способом точної перевірки типу, оскільки він використовує RuntimeTypeHandle. Оскільки точні типи в цьому випадку не збігаються з нульовими, я думаю, що is/asтут потрібно зробити додатковий важкий підйом, щоб переконатися, що він насправді є примірником типу Nullable.

І якщо чесно: що is Nullable<xxx> plus HasValueкупує тебе? Нічого. Ви завжди можете перейти безпосередньо до базового (значення) типу (у цьому випадку). Ви отримуєте значення або "ні, не екземпляр типу, про який ви просили". Навіть якщо ви написали (int?)nullв масив, перевірка типу поверне помилкову.


Цікаво ... Ідея використання "as" + HasValue (не є плюс HasValue, зауважте) полягає в тому, що вона перевіряє тип лише один раз, а не двічі. Це робить "перевірити і відключити" в один крок. Таке відчуття, що воно повинно бути швидше ... але явно це не так. Я не впевнений, що ви маєте на увазі під останнім реченням, але немає такого поняття, як коробка 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

[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

Яку рамкову версію ви використовуєте з інтересу? Результати мого нетбука (за допомогою .NET 4RC) ще більш драматичні - версії, що використовують As, набагато гірші, ніж ваші результати. Можливо, вони покращили його для .NET 4 RTM? Я все ще думаю, що це може бути швидше ...
Джон Скіт

@Michael: Ви запускали неоптимізовану збірку чи працюєте в налагоджувачі?
Джон Скіт

@Jon: неоптимізована збірка, під налагодженням
Майкл Буен,

1
@Michael: Правильно - я схильний розглядати результати роботи під налагоджувачем як неактуальні :)
Джон Скіт

@Jon: Якщо за допомогою налагоджувача, тобто всередині VS; так, попередній орієнтир робився під налагоджувачем. Я знову орієнтир, всередині VS і поза ним, і компілюється як налагодження і компілюється як реліз. Перевірте правку
Майкл Буен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.