Что быстрее, включите строку или elseif по типу?

Предположим, что у меня есть возможность идентифицировать путь кода, который нужно взять на основе сравнения строк, или iffing типа:

Что происходит быстрее и почему?

switch(childNode.Name)
{
    case "Bob":
      break;
    case "Jill":
      break;
    case "Marko":
      break;
}

if(childNode is Bob)
{
}
elseif(childNode is Jill)
{
}
else if(childNode is Marko)
{
}

Обновление: Основная причина, по которой я спрашиваю об этом, состоит в том, что оператор switch имеет особое значение для того, что считается случаем. Например, это не позволит вам использовать переменные, а только константы, которые перемещаются в основную сборку. Я предположил, что это ограничение было связано с некоторыми фанковыми вещами, которые он делал. Если это только перевод в elseifs (как комментирует один плакат), то почему мы не допускаем переменные в операторах case?

Предостережение: Я пост-оптимизирую. Этот метод вызывается много раз в медленной части приложения.

Ответ 1

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

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

Цепочка If/Else является эффективным подходом для небольшого количества сопоставлений типов или если вы можете надежно предсказать, какие из немногих типов составят большинство из тех, которые вы видите. Потенциальная проблема подхода заключается в том, что по мере увеличения количества типов число сравнений, которое должно быть выполнено, также увеличивается.

если я выполняю следующее:

int value = 25124;
if(value == 0) ...
else if (value == 1) ...
else if (value == 2) ...
...
else if (value == 25124) ... 

каждый из предыдущих условий if должен быть оценен до ввода правильного блока. С другой стороны,

switch(value) {
 case 0:...break;
 case 1:...break;
 case 2:...break;
 ...
 case 25124:...break;
}

выполнит один простой переход к правильному биту кода.

В этом примере, когда он становится более сложным, ваш другой метод использует переключатель на строках, а не целые числа, которые становятся немного более сложными. На низком уровне строки не могут включаться так же, как целочисленные значения, поэтому компилятор С# делает какую-то магию, чтобы сделать эту работу для вас.

Если оператор switch "достаточно мал" (где компилятор делает то, что, по его мнению, лучше всего автоматически), включение строк создает код, который является таким же, как цепочка if/else.

switch(someString) {
    case "Foo": DoFoo(); break;
    case "Bar": DoBar(); break;
    default: DoOther; break;
}

совпадает с:

if(someString == "Foo") {
    DoFoo();
} else if(someString == "Bar") {
    DoBar();
} else {
    DoOther();
}

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

Это выглядит примерно так (просто представьте себе больше записей, чем я буду писать)

Статическое поле определяется в "скрытом" местоположении, которое связано с классом, содержащим оператор switch типа Dictionary<string, int>, и задается искаженное имя

//Make sure the dictionary is loaded
if(theDictionary == null) { 
    //This is simplified for clarity, the actual implementation is more complex 
    // in order to ensure thread safety
    theDictionary = new Dictionary<string,int>();
    theDictionary["Foo"] = 0;
    theDictionary["Bar"] = 1;
}

int switchIndex;
if(theDictionary.TryGetValue(someString, out switchIndex)) {
    switch(switchIndex) {
    case 0: DoFoo(); break;
    case 1: DoBar(); break;
    }
} else {
    DoOther();
}

В некоторых быстрых тестах, которые я только что запускал, метод If/Else примерно в 3 раза быстрее, чем переключатель для 3 разных типов (где типы распределены случайным образом). При 25 типах коммутатор работает с небольшим отрывом (16%) при 50 типах, коммутатор более чем в два раза быстрее.

Если вы собираетесь использовать большое количество типов, я бы предложил третий метод:

private delegate void NodeHandler(ChildNode node);

static Dictionary<RuntimeTypeHandle, NodeHandler> TypeHandleSwitcher = CreateSwitcher();

private static Dictionary<RuntimeTypeHandle, NodeHandler> CreateSwitcher()
{
    var ret = new Dictionary<RuntimeTypeHandle, NodeHandler>();

    ret[typeof(Bob).TypeHandle] = HandleBob;
    ret[typeof(Jill).TypeHandle] = HandleJill;
    ret[typeof(Marko).TypeHandle] = HandleMarko;

    return ret;
}

void HandleChildNode(ChildNode node)
{
    NodeHandler handler;
    if (TaskHandleSwitcher.TryGetValue(Type.GetRuntimeType(node), out handler))
    {
        handler(node);
    }
    else
    {
        //Unexpected type...
    }
}

Это похоже на то, что предложил Тед Эллиот, но использование дескрипторов типа времени выполнения вместо объектов полного типа позволяет избежать накладных расходов на загрузку объекта типа через отражение.

Вот несколько быстрых таймингов на моей машине:

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 5 types
Method                Time    % of optimal
If/Else               179.67  100.00
TypeHandleDictionary  321.33  178.85
TypeDictionary        377.67  210.20
Switch                492.67  274.21

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 10 types
Method                Time    % of optimal
If/Else               271.33  100.00
TypeHandleDictionary  312.00  114.99
TypeDictionary        374.33  137.96
Switch                490.33  180.71

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 15 types
Method                Time    % of optimal
TypeHandleDictionary  312.00  100.00
If/Else               369.00  118.27
TypeDictionary        371.67  119.12
Switch                491.67  157.59

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 20 types
Method                Time    % of optimal
TypeHandleDictionary  335.33  100.00
TypeDictionary        373.00  111.23
If/Else               462.67  137.97
Switch                490.33  146.22

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 25 types
Method                Time    % of optimal
TypeHandleDictionary  319.33  100.00
TypeDictionary        371.00  116.18
Switch                483.00  151.25
If/Else               562.00  175.99

Testing 3 iterations with 5,000,000 data elements (mode=Random) and 50 types
Method                Time      % of optimal
TypeHandleDictionary  319.67    100.00
TypeDictionary        376.67    117.83
Switch                453.33    141.81
If/Else               1,032.67  323.04

На моей машине, по крайней мере, подход словаря дескриптора типа превосходит всех остальных для чего-либо более 15 различных типов, когда распределение типов, используемых в качестве входных данных для метода, является случайным.

Если, с другой стороны, вход полностью состоит из типа, который сначала проверяется в цепочке if/else, метод намного быстрее:

Testing 3 iterations with 5,000,000 data elements (mode=UniformFirst) and 50 types
Method                Time    % of optimal
If/Else               39.00   100.00
TypeHandleDictionary  317.33  813.68
TypeDictionary        396.00  1,015.38
Switch                403.00  1,033.33

И наоборот, если вход всегда является последним в цепочке if/else, он имеет противоположный эффект:

Testing 3 iterations with 5,000,000 data elements (mode=UniformLast) and 50 types
Method                Time      % of optimal
TypeHandleDictionary  317.67    100.00
Switch                354.33    111.54
TypeDictionary        377.67    118.89
If/Else               1,907.67  600.52

Если вы можете сделать некоторые предположения о своем вводе, вы можете получить максимальную производительность от гибридного подхода, где вы выполняете if/else проверку нескольких типов, которые наиболее распространены, а затем вернуться к подходу, основанному на словаре, если они терпят неудачу.

Ответ 2

Я только что реализовал приложение для быстрого тестирования и профилировал его с помощью ANTS 4.
Spec:.Net 3.5 sp1 в 32-разрядной Windows XP, код построен в режиме выпуска.

3 миллиона тестов:

  • Switch: 1,842 секунды
  • Если: 0.344 секунды.

Кроме того, результаты оператора switch показывают (неудивительно), что более длинные имена занимают больше времени.

1 миллион тестов

  • Боб: 0,612 секунды.
  • Джилл: 0,835 секунды.
  • Марко: 1.093 секунды.

Похоже, что "If Else" быстрее, по крайней мере, тот сценарий, который я создал.

class Program
{
    static void Main( string[] args )
    {
        Bob bob = new Bob();
        Jill jill = new Jill();
        Marko marko = new Marko();

        for( int i = 0; i < 1000000; i++ )
        {
            Test( bob );
            Test( jill );
            Test( marko );
        }
    }

    public static void Test( ChildNode childNode )
    {   
        TestSwitch( childNode );
        TestIfElse( childNode );
    }

    private static void TestIfElse( ChildNode childNode )
    {
        if( childNode is Bob ){}
        else if( childNode is Jill ){}
        else if( childNode is Marko ){}
    }

    private static void TestSwitch( ChildNode childNode )
    {
        switch( childNode.Name )
        {
            case "Bob":
                break;
            case "Jill":
                break;
            case "Marko":
                break;
        }
    }
}

class ChildNode { public string Name { get; set; } }

class Bob : ChildNode { public Bob(){ this.Name = "Bob"; }}

class Jill : ChildNode{public Jill(){this.Name = "Jill";}}

class Marko : ChildNode{public Marko(){this.Name = "Marko";}}

Ответ 3

Во-первых, вы сравниваете яблоки и апельсины. Сначала вам нужно сравнить switch on type vs switch on string, а затем if on type vs if on string, а затем сравнить победителей.

Во-вторых, это то, для чего предназначался OO. В языках, поддерживающих OO, тип включения (любого типа) является запахом кода, который указывает на плохой дизайн. Решение состоит в том, чтобы основываться на общей базе с абстрактным или виртуальным методом (или аналогичной конструкцией, в зависимости от вашего языка).

например.

class Node
{
    public virtual void Action()
    {
        // Perform default action
    }
}

class Bob : Node
{
    public override void Action()
    {
        // Perform action for Bill
    }
}

class Jill : Node
{
    public override void Action()
    {
        // Perform action for Jill
    }
}

Затем вместо выполнения инструкции switch вы вызываете childNode.Action()

Ответ 4

Оператор switch быстрее выполняется, чем лестница if-else-if. Это связано с возможностью компилятора оптимизировать оператор switch. В случае лестницы if-else-if код должен обрабатывать каждую инструкцию if в порядке, определенном программистом. Однако, поскольку каждый случай в инструкции switch не полагается на более ранние случаи, компилятор может повторно заказать тестирование таким образом, чтобы обеспечить самое быстрое выполнение.

Ответ 5

Если у вас есть классы, я бы предложил использовать шаблон стратегии Strategy вместо switch или elseif.

Ответ 6

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

Ответ 7

Если вы уже не написали это и не обнаружили проблемы с производительностью, я бы не стал беспокоиться о том, что быстрее. Пойдите с тем, что более читаемо. Помните: "Преждевременная оптимизация - это корень всего зла". - Дональд Кнут

Ответ 8

Конструкция SWITCH изначально предназначалась для целочисленных данных; он намерен использовать аргумент напрямую как индекс в "таблицу рассылки", таблицу указателей. Таким образом, был бы один тест, а затем запускаться непосредственно к соответствующему коду, а не серия тестов.

Трудность здесь состоит в том, что ее использование было обобщено на "строковые" типы, которые, очевидно, не могут использоваться в качестве индекса, и все преимущества конструкции SWITCH теряются.

Если скорость - это ваша цель, проблема не в вашем коде, а в вашей структуре данных. Если пространство имен так просто, как вы его показываете, лучше закодировать его в целочисленное значение (например, когда создаются данные) и использовать это целое число "во много раз в медленной части приложения".

Ответ 9

Если типы, которые вы используете, являются примитивными типами .NET, вы можете использовать Type.GetTypeCode(Type), но если они являются настраиваемыми типами, все они будут возвращаться как TypeCode.Object.

Также может работать словарь с делегатами или классами обработчиков.

Dictionary<Type, HandlerDelegate> handlers = new Dictionary<Type, HandlerDelegate>();
handlers[typeof(Bob)] = this.HandleBob;
handlers[typeof(Jill)] = this.HandleJill;
handlers[typeof(Marko)] = this.HandleMarko;

handlers[childNode.GetType()](childNode);
/// ...

private void HandleBob(Node childNode) {
    // code to handle Bob
}

Ответ 10

Коммутатор() будет компилировать код, эквивалентный набору else ifs. Сравнение строк будет намного медленнее, чем сравнение типов.

Ответ 11

Я вспоминаю чтение в нескольких справочниках, что ветвление if/else быстрее, чем оператор switch. Тем не менее, немного исследований в Blackwasp показывает, что оператор switch на самом деле быстрее: http://www.blackwasp.co.uk/SpeedTestIfElseSwitch.aspx

В действительности, если вы сравниваете типичные заявления от 3 до 10 (или около того), я серьезно сомневаюсь, что есть реальная прибыль от использования одного или другого.

Как уже сказал Крис, пойдите для удобочитаемости: Что происходит быстрее, включите строку или elseif по типу?

Ответ 12

Я думаю, что главная проблема с производительностью заключается в том, что в блоке switch вы сравниваете строки, а в блоке if-else вы проверяете типы... Эти два не совпадают, и поэтому я ' d сказать, что вы "сравниваете картофель с бананами".

Начну с сравнения:

switch(childNode.Name)
{
    case "Bob":
        break;
    case "Jill":
        break;
    case "Marko":
      break;
}

if(childNode.Name == "Bob")
{}
else if(childNode.Name == "Jill")
{}
else if(childNode.Name == "Marko")
{}

Ответ 13

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

interface INode
{
    void Action;
}

class Bob : INode
{
    public void Action
    {

    }
}

class Jill : INode
{
    public void Action
    {

    }
}

class Marko : INode
{
    public void Action
    {

    }
}

//Your function:
void Do(INode childNode)
{
    childNode.Action();
}

Увидеть, что делает ваш оператор switch, поможет лучше. Если ваша функция ничего не значит о действии для типа, возможно, вы можете определить перечисление для каждого типа.

enum NodeType { Bob, Jill, Marko, Default }

interface INode
{
    NodeType Node { get; };
}

class Bob : INode
{
    public NodeType Node { get { return NodeType.Bob; } }
}

class Jill : INode
{
    public NodeType Node { get { return NodeType.Jill; } }
}

class Marko : INode
{
    public NodeType Node { get { return NodeType.Marko; } }
}

//Your function:
void Do(INode childNode)
{
    switch(childNode.Node)
    {
        case Bob:
          break;
        case Jill:
          break;
        case Marko:
          break;
        Default:
          throw new ArgumentException();
    }
}

Я предполагаю, что это должно быть быстрее, чем оба подхода, о которых идет речь. Вы можете попробовать абстрактный маршрут класса если наносекунды будут важны для вас.

Ответ 14

Сравнение строк всегда будет полностью полагаться на среду выполнения (если строки не статически распределены, хотя необходимость сравнивать их друг с другом является дискуссионной). Однако сравнение типов может быть выполнено посредством динамического или статического связывания, и в любом случае оно более эффективно для среды выполнения, чем сравнение отдельных символов в строке.

Ответ 15

Конечно, коммутатор на String будет скомпилирован до сравнения строк (по одному в каждом случае), который медленнее, чем сравнение типов (и намного медленнее, чем типичное целочисленное сравнение, которое используется для switch/case)?

Ответ 16

Одна из проблем, возникающих с коммутатором, заключается в использовании строк, таких как "Боб", это приведет к гораздо большему количеству циклов и строк в скомпилированном коде. ИЛ, который сгенерирован, должен будет объявить строку, установить ее в "Боб", а затем использовать ее в сравнении. Поэтому, имея в виду, ваши операторы IF будут работать быстрее.

PS. Пример Aeon не работает, потому что вы не можете включать типы. (Нет, я не знаю, почему именно, но мы пробовали это, но это не работает. Это связано с переменным типа)

Если вы хотите проверить это, просто создайте отдельное приложение и создайте два простых метода, которые сделают то, что написано выше, и используйте что-то вроде Ildasm.exe, чтобы увидеть IL. Вы заметите намного меньше строк в IF-заявлении Method IL.

Ildasm поставляется с VisualStudio...

Страница ILDASM - http://msdn.microsoft.com/en-us/library/f7dy01k1(VS.80).aspx

ILDASM Tutorial - http://msdn.microsoft.com/en-us/library/aa309387(VS.71).aspx

Ответ 17

Три мысли:

1) Если вы собираетесь делать что-то другое, основанное на типах объектов, может возникнуть смысл переместить это поведение в эти классы. Затем вместо переключателя или if-else вы просто вызываете childNode.DoSomething().

2) Сравнение типов будет намного быстрее, чем сравнение строк.

3) В дизайне if-else вы, возможно, сможете воспользоваться переупорядочением тестов. Если объекты "Джилл" составляют 90% объектов, проходящих там, сначала проверьте их.

Ответ 18

Помните, что профилировщик - ваш друг. Любое догадки - это пустая трата времени большую часть времени. Кстати, у меня был хороший опыт работы с профилировщиком JetBrains dotTrace.

Ответ 19

Включение строки в основном скомпилируется в лестницу if-else-if. Попробуйте декомпилировать простой. В любом случае, проверка верности строк должна быть более дешевой, так как они интернированы, и все, что потребуется, - это контрольная проверка. Сделайте то, что имеет смысл с точки зрения ремонтопригодности; если вы компилируете строки, выполните строковый переключатель. Если вы выбираете на основе типа, тем более подходящей является лестница типа.

Ответ 20

Возможно, я что-то пропустил, но не мог ли вы сделать оператор switch для типа вместо String? То есть

switch(childNode.Type)
{
case Bob:
  break;
case Jill:
  break;
case Marko:
  break;
}