C # - це 'продуктивність оператора


102

У мене є програма, яка вимагає швидкої роботи. В межах одного з його внутрішніх циклів мені потрібно протестувати тип об’єкта, щоб побачити, чи він успадковується від певного інтерфейсу.

Один із способів це зробити - це вбудована функція перевірки типу CLR. Найелегантнішим методом, напевно, є ключове слово 'is':

if (obj is ISpecialType)

Іншим підходом було б надати базовому класу власну віртуальну функцію GetType (), яка повертає заздалегідь визначене значення переліку (у моєму випадку, насправді, мені потрібен лише bool). Цей метод був би швидким, але менш елегантним.

Я чув, що існує інструкція IL спеціально для ключового слова 'is', але це не означає, що вона швидко виконується при перекладі на рідну збірку. Хтось може поділитися деяким уявленням про ефективність "є" проти іншого методу?

ОНОВЛЕННЯ: Дякуємо за всі поінформовані відповіді! Здається, серед відповідей розподілено кілька корисних моментів: Ендрю про те, "чи" це автоматично виконувати акторський склад, є надзвичайно важливим, але дані про ефективність, зібрані Бінарним Вар'єром та Йеном, також надзвичайно корисні. Було б чудово, якби одна з відповідей була відредагована, щоб включити всю цю інформацію.


2
До речі, CLR не дасть вам можливості створити власну функцію Type GetType (), оскільки вона порушує одне з основних правил CLR - справді типи
abatishchev

1
Е-е, я не повністю впевнений, що ви маєте на увазі під правилом "справді типів", але я розумію, що CLR має вбудовану функцію Type GetType (). Якби я використовував цей метод, він мав би функцію іншого імені, що повертає деяке перерахування, тому не було б жодного конфлікту імені / символу.
JubJub

3
Я думаю, абатищев мав на увазі "безпеку типу". GetType () є невіртуальним, щоб запобігти тому, щоб тип брехав сам про себе і, отже, зберігаючи безпеку типу.
Andrew Hare

2
Ви розглядали можливість попереднього вибору та кешування відповідності типу, щоб вам не потрібно було робити це в межах циклів? Здається, кожне запитання про перфоманти завжди масово поставлено +1, але це просто здається мені поганим розумінням c #. Це насправді занадто повільно? Як? Що ви пробували? Очевидно, мало що давали ваші коментарі щодо відповідей ...
Гусдор

Відповіді:


114

Використання isможе погіршити продуктивність, якщо, перевіривши тип, ви перекладете на цей тип. isнасправді передає об'єкт типу, який ви перевіряєте, тому будь-яке подальше кастинг є зайвим.

Якщо ви все одно збираєтеся брати участь, ось кращий підхід:

ISpecialType t = obj as ISpecialType;

if (t != null)
{
    // use t here
}

1
Дякую. Але якщо я не збираюся кидати об’єкт, якщо умовний терпить помилку, чи буде мені краще використовувати віртуальну функцію для тестування типу?
JubJub

4
@JubJub: ні. Помилка в asосновному виконує ту ж операцію, що і is(а саме перевірку типу). Єдина різниця полягає в тому, що він потім повертається nullзамість false.
Конрад Рудольф

74

Я з Йеном , ти, мабуть, не хочеш цього робити.

Однак, щоб ви знали, різниця між цими двома - понад 10 000 000 ітерацій - дуже мала

  • Перевірка переліку відбувається за 700 мілісекунд (приблизно)
  • Перевірка IS відбувається через 1000 мілісекунд (приблизно)

Я особисто не вирішив би цю проблему таким чином, але якби мене змусили вибрати один метод, це була б вбудована перевірка ІС, різниця в продуктивності не варто розглядати накладні витрати на кодування.

Мої базові та похідні класи

class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
}

class MyClassA : MyBaseClass
{
    public MyClassA()
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
    }
}
class MyClassB : MyBaseClass
{
    public MyClassB()
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
    }
}

JubJub: На запит більше інформації про тести.

Я провів обидва тести з консольного додатка (збірка), кожен тест виглядає наступним чином

static void IsTest()
{
    DateTime start = DateTime.Now;
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass a;
        if (i % 2 == 0)
            a = new MyClassA();
        else
            a = new MyClassB();
        bool b = a is MyClassB;
    }
    DateTime end = DateTime.Now;
    Console.WriteLine("Is test {0} miliseconds", (end - start).TotalMilliseconds);
}

Запускаючи реліз, я отримую різницю в 60 - 70 мс, як Ян.

Подальше оновлення - 25 жовтня 2012 р.
Через кілька років тому я щось помітив про це, компілятор може вибрати пропустити bool b = a is MyClassBу випуску, оскільки b ніде не використовується.

Цей код. . .

public static void IsTest()
{
    long total = 0;
    var a = new MyClassA();
    var b = new MyClassB();
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass baseRef;
        if (i % 2 == 0)
            baseRef = a;//new MyClassA();
        else
            baseRef = b;// new MyClassB();
        //bool bo = baseRef is MyClassB;
        bool bo = baseRef.ClassType == MyBaseClass.ClassTypeEnum.B;
        if (bo) total += 1;
    }
    sw.Stop();
    Console.WriteLine("Is test {0} miliseconds {1}", sw.ElapsedMilliseconds, total);
}

. . . послідовно показує, що isперевірка надходить приблизно через 57 мілісекунд, а порівняння переліку - 29 мілісекунд.

Примітка: Я б все-таки віддав перевагу isчеку, різниця занадто мала, щоб турбуватися про неї


35
+1 для фактичного тестування продуктивності, а не для припущення.
Джон Такбері

3
Набагато краще робити тест за допомогою секундоміра, а не DateTime.
Зараз

2
Я візьму це до уваги, однак у цьому випадку я не думаю, що це вплине на результат. Дякую :)
Binary Worrier

11
@Binary Worrier - Ваш новий розподіл класів оператором повністю затьмарить будь-які відмінності в продуктивності в операціях 'є'. Чому б вам не видалити ці нові операції, повторно використовуючи два різні попередньо виділені екземпляри, а потім повторно запустити код і опублікувати результати.

1
@mcmillab: Я гарантую, що, що б ти не робив, ти матимеш вузькі місця на багато порядків більше, ніж будь-яке погіршення продуктивності, isяке спричиняє тобі оператор, і що надмірно почуте проектування та кодування навколо isоператора буде коштувати ціле багатство в якість коду, і в кінцевому підсумку також буде розумною. У цьому випадку я дотримуюсь свого твердження. «Є» оператор не ніколи не буде проблема з продуктивністю виконання.
Binary Worrier

23

Гаразд, я поспілкувався з кимось про це і вирішив перевірити це ще. Наскільки я можу зрозуміти, продуктивність asі isє дуже хорошими, порівняно з тестуванням власного члена або функції для зберігання інформації про тип.

Я використовував Stopwatch, що я щойно дізнався, можливо, не найнадійніший підхід, тому я також спробував UtcNow. Пізніше я також спробував підхід до часу процесора, який, схоже, UtcNowвключає непередбачуваний час створення. Я також намагався зробити базовий клас абстрактним без віртуальних програм, але, здається, це не мало суттєвого ефекту.

Я провів це на Quad Q6600 з 16 Гб оперативної пам'яті. Навіть при 50-міліметрових ітераціях цифри все одно відхиляються навколо +/- 50 або близько мілісекунд, тому я б не читав занадто багато про незначні відмінності.

Було цікаво побачити, що x64 створюється швидше, але виконується як / повільніше, ніж x86

x64 Режим випуску:
Секундомір:
Як: 561 мс
: 597 мс
Базова властивість: 539 мс
Базове поле: 555 мс
Базове поле RO: 552 мс
Віртуальний тест GetEnumType (): 556 мс
Віртуальний тест IsB (): 588 мс
Час створення: 10416 мс

UtcNow:
As: 499ms
Is: 532ms
Базова властивість: 479ms
Базове поле: 502ms
Базове поле RO: 491ms
Virtual GetEnumType (): 502ms
Virtual bool IsB (): 522ms
Час створення: 285ms (Це число здається ненадійним для UtcNow. Я також отримую 109ms і 806 мс.)

x86 Режим випуску:
Секундомір:
Як: 391 мс
: 423 мс
Базова властивість: 369 мс
Базове поле: 321 мс
Базове поле RO: 339 мс
Віртуальний тест GetEnumType (): 361 мс
Віртуальний тест IsB (): 365 мс
Час створення: 14106 мс

UtcNow:
As: 348ms
Is: 375ms
Базова властивість: 329ms
Базове поле: 286ms
Базове поле RO: 309ms
Virtual GetEnumType (): 321ms
Virtual bool IsB (): 332ms
Час створення: 544ms (Це число здається ненадійним для UtcNow.)

Ось більшість коду:

    static readonly int iterations = 50000000;
    void IsTest()
    {
        Process.GetCurrentProcess().ProcessorAffinity = (IntPtr)1;
        MyBaseClass[] bases = new MyBaseClass[iterations];
        bool[] results1 = new bool[iterations];

        Stopwatch createTime = new Stopwatch();
        createTime.Start();
        DateTime createStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            if (i % 2 == 0) bases[i] = new MyClassA();
            else bases[i] = new MyClassB();
        }
        DateTime createStop = DateTime.UtcNow;
        createTime.Stop();


        Stopwatch isTimer = new Stopwatch();
        isTimer.Start();
        DateTime isStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] =  bases[i] is MyClassB;
        }
        DateTime isStop = DateTime.UtcNow; 
        isTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch asTimer = new Stopwatch();
        asTimer.Start();
        DateTime asStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i] as MyClassB != null;
        }
        DateTime asStop = DateTime.UtcNow; 
        asTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch baseMemberTime = new Stopwatch();
        baseMemberTime.Start();
        DateTime baseStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassType == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseStop = DateTime.UtcNow;
        baseMemberTime.Stop();
        CheckResults(ref  results1);

        Stopwatch baseFieldTime = new Stopwatch();
        baseFieldTime.Start();
        DateTime baseFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseFieldStop = DateTime.UtcNow;
        baseFieldTime.Stop();
        CheckResults(ref  results1);


        Stopwatch baseROFieldTime = new Stopwatch();
        baseROFieldTime.Start();
        DateTime baseROFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseROFieldStop = DateTime.UtcNow;
        baseROFieldTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethTime = new Stopwatch();
        virtMethTime.Start();
        DateTime virtStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].GetClassType() == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime virtStop = DateTime.UtcNow;
        virtMethTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethBoolTime = new Stopwatch();
        virtMethBoolTime.Start();
        DateTime virtBoolStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].IsB();
        }
        DateTime virtBoolStop = DateTime.UtcNow;
        virtMethBoolTime.Stop();
        CheckResults(ref  results1);


        asdf.Text +=
        "Stopwatch: " + Environment.NewLine 
          +   "As:  " + asTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           +"Is:  " + isTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           + "Base property:  " + baseMemberTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base field:  " + baseFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base RO field:  " + baseROFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType() test:  " + virtMethTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual IsB() test:  " + virtMethBoolTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Create Time :  " + createTime.ElapsedMilliseconds + "ms" + Environment.NewLine + Environment.NewLine+"UtcNow: " + Environment.NewLine + "As:  " + (asStop - asStart).Milliseconds + "ms" + Environment.NewLine + "Is:  " + (isStop - isStart).Milliseconds + "ms" + Environment.NewLine + "Base property:  " + (baseStop - baseStart).Milliseconds + "ms" + Environment.NewLine + "Base field:  " + (baseFieldStop - baseFieldStart).Milliseconds + "ms" + Environment.NewLine + "Base RO field:  " + (baseROFieldStop - baseROFieldStart).Milliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType():  " + (virtStop - virtStart).Milliseconds + "ms" + Environment.NewLine + "Virtual bool IsB():  " + (virtBoolStop - virtBoolStart).Milliseconds + "ms" + Environment.NewLine + "Create Time :  " + (createStop-createStart).Milliseconds + "ms" + Environment.NewLine;
    }
}

abstract class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
    public ClassTypeEnum ClassTypeField;
    public readonly ClassTypeEnum ClassTypeReadonlyField;
    public abstract ClassTypeEnum GetClassType();
    public abstract bool IsB();
    protected MyBaseClass(ClassTypeEnum kind)
    {
        ClassTypeReadonlyField = kind;
    }
}

class MyClassA : MyBaseClass
{
    public override bool IsB() { return false; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.A; }
    public MyClassA() : base(MyBaseClass.ClassTypeEnum.A)
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
        ClassTypeField = MyBaseClass.ClassTypeEnum.A;            
    }
}
class MyClassB : MyBaseClass
{
    public override bool IsB() { return true; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.B; }
    public MyClassB() : base(MyBaseClass.ClassTypeEnum.B)
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
        ClassTypeField = MyBaseClass.ClassTypeEnum.B;
    }
}

45
(Деякий бонус 5 натхненних Шекспіром ...) Бути чи не бути: ось у чому питання: чи благородніше в коді страждати від переліків та властивостей абстрактних основ, чи взяти пропозиції посередника мовознавець І, посилаючись на його вказівки, довіряти їм? Вгадувати: дивуватися; Не більше; і за часом розпізнавання ми припиняємо головний біль і тисячу підсвідомих роздумів, яким є спадкоємці обмежених часом кодерів. 'Це закриття Побожно, на побажання. Померти, ні, але спати; Так, я буду спати, можливо, про що мріяти, і як про те, що може бути виведено з самої основи класу.
Джаред Тірск,

Чи можна з цього зробити висновок, що доступ до властивості швидший на x64, ніж доступ до поля !!! Тому що для мене це надзвичайно несподівано, як це може бути?
Дідьє А.

1
Я б не робив такого висновку, оскільки: "Навіть при 50-міліметрових ітераціях цифри все одно відхиляються навколо +/- 50 або близько мілісекунд, щоб я не читав занадто багато про незначні відмінності".
Джаред Тірск,

16

Андрій прав. Насправді при аналізі коду Visual Studio повідомляє про це як про непотрібний склад.

Одна ідея (не знаючи, що ти робиш, це трохи пострілу в темряві), але мені завжди рекомендували уникати таких перевірок, а замість цього мати інший клас. Тож замість того, щоб робити деякі перевірки та виконувати різні дії залежно від типу, дайте класу знати, як сам обробляти ...

наприклад, Obj може бути ISpecialType або IType;

обидва вони мають визначений метод DoStuff (). Для IType він може просто повертати або робити власні речі, тоді як ISpecialType може робити інші речі.

Потім це повністю видаляє будь-який кастинг, робить код чистішим та простішим у обслуговуванні, а клас знає, як виконувати власні завдання.


Так, оскільки все, що я збираюся зробити, якщо перевірка типу true - це виклик певного методу інтерфейсу, я міг би просто перенести цей метод інтерфейсу в базовий клас і не робити нічого за замовчуванням. Це може бути більш елегантно, ніж створення віртуальної функції для тестування типу.
JubJub

Я провів аналогічний тест з Binary Worrier після коментарів Абатіщева і виявив лише 60 мс різниці в 10 000 000 ітерацій.
Ян

1
Ого, дякую за допомогу. Я гадаю, що поки я дотримуватимусь використання операторів перевірки типу, поки це не здається доречним реорганізувати структуру класу. Я буду використовувати оператор "як", як запропонував Ендрю, оскільки я не хочу робити надлишки.
JubJub

15

Я зробив порівняння продуктивності щодо двох можливостей порівняння типів

  1. myobject.GetType () == typeof (MyClass)
  2. myobject - це MyClass

Результат: Використання "є" приблизно в 10 разів швидше !!!

Вихід:

Час для порівняння типів: 00: 00: 00.456

Час для порівняння Is: 00: 00: 00.042

Мій код:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ConsoleApplication3
{
    class MyClass
    {
        double foo = 1.23;
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass myobj = new MyClass();
            int n = 10000000;

            Stopwatch sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj.GetType() == typeof(MyClass);
            }

            sw.Stop();
            Console.WriteLine("Time for Type-Comparison: " + GetElapsedString(sw));

            sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj is MyClass;
            }

            sw.Stop();
            Console.WriteLine("Time for Is-Comparison: " + GetElapsedString(sw));
        }

        public static string GetElapsedString(Stopwatch sw)
        {
            TimeSpan ts = sw.Elapsed;
            return String.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
        }
    }
}

13

Точка Ендрю Заєць розповів про втрату продуктивності, коли ви виконуєте isперевірку, а потім прив'язка була дійсною, але в C # 7.0 ми можемо зробити, це перевірити відповідність шаблону відьми, щоб уникнути додаткового приведення пізніше:

if (obj is ISpecialType st)
{
   //st is in scope here and can be used
}

Більше того, якщо вам потрібно перевірити між кількома типами конструкцій збігу шаблонів C # 7.0, тепер ви можете робити switchз типами:

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

Детальніше про зіставлення шаблонів у C # ви можете прочитати в документації тут .


1
Звичайно, дійсне рішення, але ця функція зіставлення зразків C # мене засмучує, коли вона заохочує такий код, як "заздрість до особливостей". Безумовно, ми повинні прагнути до інкапсуляції логіки, коли лише похідні об'єкти "знають", як обчислити власну площу, а потім вони просто повертають значення?
Dib

2
SO потребує кнопок фільтра (щодо запитання) для відповідей, які стосуються новіших версій фреймворку, платформи тощо. Ця відповідь лежить в основі правильної для C # 7.
Нік Вестгейт

1
Ідеали @Dib OOP викидаються з вікна при роботі з типами / класами / інтерфейсами, якими ви не керуєте. Цей підхід також корисний при обробці результату функції, яка може повертати одне з багатьох значень абсолютно різних типів (оскільки C # ще не підтримує типи об'єднання - ви можете використовувати бібліотеки типу, OneOf<T...>але вони мають основні недоліки) .
Дай

4

Якщо хтось цікавиться, я провів тести в Unity engine 2017.1 із сценарієм виконання .NET4.6 (Experimantal) на ноутбуці з процесором i5-4200U. Результати:

Average Relative To Local Call LocalCall 117.33 1.00 is 241.67 2.06 Enum 139.33 1.19 VCall 294.33 2.51 GetType 276.00 2.35

Повна стаття: http://www.ennoble-studios.com/tuts/unity-c-performance-comparison-is-vs-enum-vs-virtual-call.html


Посилання на статтю мертве.
James Wilkins

Посилання @James відроджено.
Гру

Хороші речі - але я не голосував проти вас (насправді я все одно голосував); Якщо вам цікаво. :)
Джеймс Уілкінс,

-3

Мені завжди рекомендували уникати таких перевірок, а замість цього мати інший клас. Тож замість того, щоб робити деякі перевірки та виконувати різні дії залежно від типу, дайте класу знати, як сам обробляти ...

наприклад, Obj може бути ISpecialType або IType;

обидва вони мають визначений метод DoStuff (). Для IType він може просто повертати або робити власні речі, тоді як ISpecialType може робити інші речі.

Потім це повністю видаляє будь-який кастинг, робить код чистішим та простішим у обслуговуванні, а клас знає, як виконувати власні завдання.


1
Це не дає відповіді на запитання. У будь-якому випадку, класи можуть не завжди знати, як обробляти себе через відсутність контексту. Ми застосовуємо подібну логіку до обробки винятків, коли дозволяємо виняткам йти вгору по ланцюжку викликів, поки якийсь метод / функція не має достатнього контексту для обробки помилок.
Вахтанг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.