Что быстрее, включить строку или еще, если тип? - PullRequest
73 голосов
/ 18 сентября 2008

Допустим, у меня есть опция определения пути к коду на основе сравнения строк или же типа 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?

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

Ответы [ 23 ]

124 голосов
/ 24 сентября 2008

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

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

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

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

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

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

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

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

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

Если оператор 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

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

18 голосов
/ 18 сентября 2008

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

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

  • Переключатель: 1,842 секунды
  • Если: 0,344 секунды.

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

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

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

Я выгляжу так, как будто «Если иначе» быстрее, по крайней мере, сценарий, который я создал.

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";}}
17 голосов
/ 18 сентября 2008

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

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

например.

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 ()

12 голосов
/ 18 сентября 2008

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

6 голосов
/ 18 сентября 2008

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

4 голосов
/ 18 сентября 2008

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

3 голосов
/ 18 сентября 2008

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

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

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

3 голосов
/ 18 сентября 2008

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

3 голосов
/ 18 сентября 2008

Если типы, которые вы включаете, являются примитивными типами .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
}
2 голосов
/ 18 сентября 2008

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

Я бы начал со сравнения:

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")
{}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...