C # производительность оператора is

102

У меня есть программа, требующая быстрой работы. В одном из его внутренних циклов мне нужно проверить тип объекта, чтобы увидеть, наследуется ли он от определенного интерфейса.

Один из способов сделать это - использовать встроенную в CLR функцию проверки типов. Самый элегантный метод, вероятно, это ключевое слово is:

if (obj is ISpecialType)

Другой подход - предоставить базовому классу мою собственную виртуальную функцию GetType (), которая возвращает предопределенное значение перечисления (в моем случае на самом деле мне нужен только bool). Этот метод будет быстрым, но менее элегантным.

Я слышал, что есть инструкция IL специально для ключевого слова is, но это не значит, что она выполняется быстро при переводе в собственную сборку. Может ли кто-нибудь поделиться некоторыми взглядами на эффективность «есть» по сравнению с другим методом?

ОБНОВЛЕНИЕ: Спасибо за все осознанные ответы! Кажется, в ответах разбросано несколько полезных моментов: точка зрения Эндрю об автоматическом выполнении преобразования важна, но данные о производительности, собранные Binary Worrier и Яном, также чрезвычайно полезны. Было бы здорово, если бы один из ответов был отредактирован, чтобы включить всю эту информацию.

JubJub
источник
2
кстати, CLR не даст вам возможности создать свою собственную функцию Type GetType (), потому что она нарушает одно из основных правил CLR
истинные
1
Э, я не совсем уверен, что вы имеете в виду под правилом «истинно типы», но я понимаю, что в CLR есть встроенная функция Type GetType (). Если бы я использовал этот метод, это было бы с функцией с другим именем, возвращающей некоторое перечисление, поэтому не было бы конфликта имени / символа.
JubJub
3
Я думаю, что Абатищев имел в виду «безопасность типа». GetType () не является виртуальным, чтобы тип не лгал о себе и, следовательно, сохранял безопасность типа.
Эндрю Хэйр,
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 миллисекунд (приблизительно)

Я лично не стал бы исправлять эту проблему таким образом, но если бы мне пришлось выбрать один метод, это была бы встроенная проверка IS, разница в производительности не стоит того, чтобы учитывать накладные расходы на кодирование.

Мои базовые и производные классы

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 миллисекунд, а сравнение enum происходит через 29 миллисекунд.

NB, я бы предпочел isчек, разница слишком мала, чтобы о ней заботиться

Бинарная тревога
источник
35
+1 за фактическое тестирование производительности, а не за предположение.
Джон Тэкабери,
3
Намного лучше провести тест с классом Stopwatch, а не с DateTime.Now, что очень дорого,
Абатищев
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:
Секундомер:
As: 561 мс
Is: 597 мс
Базовое свойство: 539 мс
Базовое поле: 555 мс
Базовое поле RO: 552 мс
Виртуальный тест GetEnumType (): 556
мс Тест виртуального IsB (): 588 мс
Время создания: 10416 мс

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

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

UtcNow:
As: 348 мс
Is: 375 мс
Базовое свойство: 329 мс
Базовое поле: 286 мс
Базовое поле RO: 309 мс
Виртуальный GetEnumType (): 321 мс
Virtual bool IsB (): 332 мс
Время создания: 544 мс (Это число кажется ненадежным с 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 может делать другие вещи.

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

Ян
источник
Да, поскольку все, что я собираюсь сделать, если проверка типа истинна, - это вызвать на нем определенный метод интерфейса, я мог бы просто переместить этот метод интерфейса в базовый класс и по умолчанию он ничего не делает. Это может быть более элегантно, чем создание виртуальной функции для проверки типа.
JubJub
Я провел аналогичный тест с Binary Worrier после комментариев Абатищева и обнаружил разницу всего в 60 мс на 10 000 000 итераций.
Ян,
1
Вау, спасибо за помощь. Полагаю, что пока я буду придерживаться операторов проверки типов, если только это не кажется целесообразным для реорганизации структуры классов. Я буду использовать оператор «as», как предложил Эндрю, поскольку я не хочу выполнять избыточное приведение.
JubJub
15

Я провел сравнение производительности двух вариантов сравнения типов

  1. myobject.GetType () == typeof (MyClass)
  2. myobject - это MyClass

Результат: Использование "is" примерно в 10 раз быстрее !!!

Вывод:

Время для сравнения типов: 00: 00: 00.456

Время для сравнения: 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);
        }
    }
}
Knasterbax
источник
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 # меня огорчает, когда она поощряет такой код "зависти к функциям". Разумеется, мы должны стремиться к инкапсуляции логики, при которой только производные объекты «знают», как вычислить свою площадь, а затем они просто возвращают значение?
Диб
2
SO нужны кнопки фильтра (в вопросе) для ответов, которые относятся к более новым версиям фреймворка, платформы и т. Д. Этот ответ составляет основу правильного ответа для C # 7.
Ник Вестгейт
1
Идеалы ООП @Dib выбрасываются из окна, когда вы работаете с типами / классами / интерфейсами, которые вы не контролируете. Этот подход также полезен при обработке результата функции, которая может возвращать одно из многих значений совершенно разных типов (поскольку C # еще не поддерживает типы объединения - вы можете использовать такие библиотеки, OneOf<T...>но у них есть серьезные недостатки) .
Дай
5

Если кому-то интересно, я провел тесты в движке Unity 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 ожила.
Gru
Хороший материал - но я не голосовал против вас (на самом деле я все равно голосовал за); Если вам интересно. :)
Джеймс Уилкинс
-3

Мне всегда советовали избегать такой проверки и вместо этого иметь другой класс. Поэтому вместо того, чтобы выполнять некоторые проверки и выполнять разные действия в зависимости от типа, пусть класс знает, как обрабатывать себя ...

например, Obj может иметь значение ISpecialType или IType;

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

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

user3802787
источник
1
Это не отвечает на вопрос. В любом случае классы могут не всегда знать, как себя обрабатывать из-за отсутствия контекста. Мы применяем аналогичную логику к обработке исключений, когда разрешаем исключениям подниматься по цепочке вызовов до тех пор, пока какой-либо метод / функция не получит достаточно контекста для обработки ошибок.
Вахтанг