Как бы вы сделали это выражение оператора как можно быстрее?

2009-12-04 UPDATE: для профилирования результатов по ряду предложений, размещенных здесь, см. ниже!


Вопрос

Рассмотрим следующий очень безобидный, очень простой метод, который использует оператор switch для возврата определенного значения перечисления:

public static MarketDataExchange GetMarketDataExchange(string ActivCode) {
    if (ActivCode == null) return MarketDataExchange.NONE;

    switch (ActivCode) {
        case "": return MarketDataExchange.NBBO;
        case "A": return MarketDataExchange.AMEX;
        case "B": return MarketDataExchange.BSE;
        case "BT": return MarketDataExchange.BATS;
        case "C": return MarketDataExchange.NSE;
        case "MW": return MarketDataExchange.CHX;
        case "N": return MarketDataExchange.NYSE;
        case "PA": return MarketDataExchange.ARCA;
        case "Q": return MarketDataExchange.NASDAQ;
        case "QD": return MarketDataExchange.NASDAQ_ADF;
        case "W": return MarketDataExchange.CBOE;
        case "X": return MarketDataExchange.PHLX;
        case "Y": return MarketDataExchange.DIRECTEDGE;
    }

    return MarketDataExchange.NONE;
}

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

С самого начала, позвольте мне просто предложить быстрое выражение об отказе от ответственности: это для забавы, а не для того, чтобы подпитывать все "оптимизировать или не оптимизировать" дебаты. Тем не менее, если вы считаете себя среди тех, кто догматично полагает, что "преждевременная оптимизация - корень всего зла", просто отдавайте себе отчет в том, что я работаю в высокочастотной торговой фирме, где все должно работать как можно быстрее - узкое место или нет. Итак, хотя я размещаю это на SO для fun, это не просто огромная трата времени.

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


Результаты

Хорошо, оказывается, что самое быстрое решение, которое я тестировал, произошло от João Angelo, предложение которого было на самом деле очень простым, но чрезвычайно умным. Решение, которое мой коллега и я разработали (после опробования нескольких подходов, многие из которых были придуманы здесь), заняли второе место; Я собирался опубликовать его, но оказывается, что Марк Рэнсом придумал ту же самую идею, поэтому просто проверьте его ответ!

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

Я проверил эти тесты на двух разных машинах: мой персональный компьютер дома (двухъядерный Athlon с 4 Гб оперативной памятью под управлением Windows 7 64-бит) и моя машина разработки на работе (двухъядерный Athlon с 2 Гб оперативной памяти под управлением Windows XP SP3). Очевидно, времена были разные; однако относительные времена, означающие, как каждый метод сравнивался с любым другим методом, были одинаковыми. То есть, самый быстрый был самым быстрым на обеих машинах и т.д.

Теперь о результатах. (Время, которое я публикую ниже, находится на моем домашнем компьютере.)

Но сначала для справки - исходный оператор switch:
1000000 пробегов: 98,88 мс
Средний: 0.09888 микросекунд

Самая быстрая оптимизация:

  • Идея João Angelo о назначении значений перечислениям на основе хеш-кодов строк ActivCode, а затем непосредственно обтекание ActivCode.GetHashCode() до MarketDataExchange:
    1000000 пробегов: 23,64 мс
    Средний балл: 0.02364 микросекунд
    Увеличение скорости: 329.90%

  • Мой коллега и моя идея лить ActivCode[0] в int и получить соответствующий MarketDataExchange из массива, инициализированного при запуске (эта же идея была предложена Марком Рэнсом):
    1000000 пробегов: 28,76 мс
    Средний балл: 0,02876 микросекунд
    Увеличение скорости: 253,13%

  • tster идея включения вывода ActivCode.GetHashCode() вместо ActivCode:
    1000000 пробегов: 34,69 мс
    Средний: 0.03469 микросекунды
    Увеличение скорости: 185,04%

  • Идея, предложенная несколькими пользователями, включая Auraseer, tster и kyoryu, включить ActivCode[0] вместо ActivCode:
    1000000 пробегов: 36,57 мс
    Средний балл: 0.03657 микросекунд
    Увеличение скорости: 174.66%

  • Идея Loadmaster использования быстрого хеша, ActivCode[0] + ActivCode[1]*0x100:
    1000000 пробегов: 39,53 мс
    Средний балл: 0.03953 microsecond
    Увеличение скорости: 153.53%

  • Использование хэш-таблицы (Dictionary<string, MarketDataExchange>), как полагают многие:
    1000000 пробегов: 88,32 мс
    Средний: 0.08832 микросекунд
    Увеличение скорости: 12,36%

  • Использование двоичного поиска:
    1000000 пробегов: 1031 мс
    Средний: 1.031 микросекунды
    Увеличение скорости: нет (ухудшение характеристик)

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

Ответ 1

Предполагая, что каждый вход будет действительным ActivCode, вы можете изменить значения перечисления и сильно связать с реализацией GetHashCode:

enum MarketDataExchange
{
    NONE,
    NBBO = 371857150,
    AMEX = 372029405,
    BSE = 372029408,
    BATS = -1850320644,
    NSE = 372029407,
    CHX = -284236702,
    NYSE = 372029412,
    ARCA = -734575383,
    NASDAQ = 372029421,
    NASDAQ_ADF = -1137859911,
    CBOE = 372029419,
    PHLX = 372029430,
    DIRECTEDGE = 372029429
}

public static MarketDataExchange GetMarketDataExchange(string ActivCode)
{
    if (ActivCode == null) return MarketDataExchange.NONE;

    return (MarketDataExchange)ActivCode.GetHashCode();
}

Ответ 2

Я бы свернул свою собственную функцию быстрого хэша и использовал оператор switch integer, чтобы избежать сравнения строк:

int  h = 0;  

// Compute fast hash: A[0] + A[1]*0x100
if (ActivCode.Length > 0)
    h += (int) ActivCode[0];
if (ActivCode.Length > 1)
    h += (int) ActivCode[1] << 8;  

// Find a match
switch (h)
{
    case 0x0000:  return MarketDataExchange.NBBO;        // ""
    case 0x0041:  return MarketDataExchange.AMEX;        // "A"
    case 0x0042:  return MarketDataExchange.BSE;         // "B"
    case 0x5442:  return MarketDataExchange.BATS;        // "BT"
    case 0x0043:  return MarketDataExchange.NSE;         // "C"
    case 0x574D:  return MarketDataExchange.CHX;         // "MW"
    case 0x004E:  return MarketDataExchange.NYSE;        // "N"
    case 0x4150:  return MarketDataExchange.ARCA;        // "PA"
    case 0x0051:  return MarketDataExchange.NASDAQ;      // "Q"
    case 0x4451:  return MarketDataExchange.NASDAQ_ADF;  // "QD"
    case 0x0057:  return MarketDataExchange.CBOE;        // "W"
    case 0x0058:  return MarketDataExchange.PHLX;        // "X"
    case 0x0059:  return MarketDataExchange.DIRECTEDGE;  // "Y"
    default:      return MarketDataExchange.NONE;
}

Мои тесты показывают, что это примерно в 4,5 раза быстрее, чем исходный код.

Если у С# был препроцессор, я бы использовал макрос для формирования констант case.

Этот метод быстрее, чем использование хеш-таблицы и, конечно, быстрее, чем использование сравнения строк. Он работает для четырехсимвольных строк с 32-битными int и до 8 символов с использованием 64-битных длин.

Ответ 3

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

Предполагая, что ActivCode всегда действителен, конечно, ускорит работу. Вам не нужно проверять значение null или пустую строку, и вы можете оставить один тест с конца коммутатора. То есть, проверьте все, кроме Y, и затем верните DIRECTEDGE, если совпадение не найдено.

Вместо включения всей строки, включите ее первую букву. Для кодов, имеющих больше букв, введите второй тест внутри корпуса коммутатора. Что-то вроде этого:

switch(ActivCode[0])
{
   //etc.
   case 'B':
      if ( ActivCode.Length == 1 ) return MarketDataExchange.BSE; 
      else return MarketDataExchange.BATS;
      // etc.
}

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

Ответ 4

Я бы использовал словарь для пар значений ключа и использовал время поиска O (1).

Ответ 5

Есть ли у вас статистические данные о том, какие строки более распространены? Чтобы сначала проверить их?

Ответ 6

С допустимым входом можно использовать

if (ActivCode.Length == 0)
    return MarketDataExchange.NBBO;

if (ActivCode.Length == 1)
    return (MarketDataExchange) (ActivCode[0]);

return (MarketDataExchange) (ActivCode[0] | ActivCode[1] << 8);

Ответ 7

Измените переключатель, чтобы включить HashCode() для строк.

Ответ 8

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

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

(int)ActivCode[0]*2 + ActivCode.Length-1

Для этого потребуется таблица из 51 элемента, легко хранимая в кеше L1, в следующих предположениях:

  • Входные данные уже должны быть проверены
  • пустая строка должна обрабатываться отдельно
  • никакие двухсимвольные коды не начинаются с того же символа
  • добавление новых случаев затруднено

случай пустой строки может быть включен, если вы можете использовать небезопасный доступ к ActivCode[0], дающий терминатор '\ 0'.

Ответ 9

Простите меня, если я что-то не так понял, я экстраполирую свое знание С++. Например, если вы берете ActivCode [0] пустой строки, в С++ вы получаете символ, значение которого равно нулю.

Создайте двухмерный массив, который вы инициализируете один раз; первое измерение - длина кода, второе - значение символа. Заполните нумерацию, которую вы хотите вернуть. Теперь вся ваша функция будет:

public static MarketDataExchange GetMarketDataExchange(string ActivCode) {
    return LookupTable[ActivCode.Length][ActivCode[0]];
}

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

Ответ 10

Я бы поставил его в словаре вместо использования оператора switch. Это, как говорится, может не иметь значения. Или это может быть. См. ограничения оператора С# - почему?.

Ответ 11

  • Избегайте всех сопоставлений строк.
  • Избегайте просмотра более одного символа (когда-либо)
  • Избегайте if-else, так как я хочу, чтобы компилятор мог оптимизировать лучшее, что может
  • Попробуйте получить результат в одном прыжке с коммутатором

код:

public static MarketDataExchange GetMarketDataExchange(string ActivCode) {
    if (ActivCode == null) return MarketDataExchange.NONE;
    int length = ActivCode.Length;
    if (length == 0) return MarketDataExchange.NBBO;

    switch (ActivCode[0]) {
        case 'A': return MarketDataExchange.AMEX;
        case 'B': return (length == 2) ? MarketDataExchange.BATS : MarketDataExchange.BSE;
        case 'C': return MarketDataExchange.NSE;
        case 'M': return MarketDataExchange.CHX;
        case 'N': return MarketDataExchange.NYSE;
        case 'P': return MarketDataExchange.ARCA;
        case 'Q': return (length == 2) ? MarketDataExchange.NASDAQ_ADF : MarketDataExchange.NASDAQ;
        case 'W': return MarketDataExchange.CBOE;
        case 'X': return MarketDataExchange.PHLX;
        case 'Y': return MarketDataExchange.DIRECTEDGE;
        default:  return MarketDataExchange.NONE;
    }
}

Ответ 12

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

public class Service 
{
    public static MarketDataExchange GetMarketDataExchange(string ActivCode) {
    {
        int x = 65, y = 65;
        switch(ActivCode.Length)
        {
            case 1:
                x = ActivCode[0];
                break;
            case 2:
                x = ActivCode[0];
                y = ActivCode[1];
                break;
        }
        return _table[x, y];
    }

    static Service()
    {
        InitTable();
    }

    public static MarketDataExchange[,] _table = 
        new MarketDataExchange['Z','Z'];

    public static void InitTable()
    {
        for (int x = 0; x < 'Z'; x++)
            for (int y = 0; y < 'Z'; y++)
                _table[x, y] = MarketDataExchange.NONE;

        SetCell("", MarketDataExchange.NBBO);
        SetCell("A", MarketDataExchange.AMEX);
        SetCell("B", MarketDataExchange.BSE);
        SetCell("BT", MarketDataExchange.BATS);
        SetCell("C", MarketDataExchange.NSE);
        SetCell("MW", MarketDataExchange.CHX);
        SetCell("N", MarketDataExchange.NYSE);
        SetCell("PA", MarketDataExchange.ARCA);
        SetCell("Q", MarketDataExchange.NASDAQ);
        SetCell("QD", MarketDataExchange.NASDAQ_ADF);
        SetCell("W", MarketDataExchange.CBOE);
        SetCell("X", MarketDataExchange.PHLX);
        SetCell("Y", MarketDataExchange.DIRECTEDGE);
    }

    private static void SetCell(string s, MarketDataExchange exchange)
    {
        char x = 'A', y = 'A';
        switch(s.Length)
        {
            case 1:
                x = s[0];
                break;
            case 2:
                x = s[0];
                y = s[1];
                break;
        }
        _table[x, y] = exchange;
    }
}

Создайте байт на основе перечисления, чтобы сохранить небольшое пространство.

public enum MarketDataExchange : byte
{
    NBBO, AMEX, BSE, BATS, NSE, CHX, NYSE, ARCA, 
    NASDAQ, NASDAQ_ADF, CBOE, PHLIX, DIRECTEDGE, NONE
}

Ответ 13

Если значения перечисления произвольны, вы можете сделать это...

public static MarketDataExchange GetValue(string input)
{
    switch (input.Length)
    {
        case 0: return MarketDataExchange.NBBO;
        case 1: return (MarketDataExchange)input[0];
        case 2: return (MarketDataExchange)(input[0] << 8 | input[1]);
        default: return MarketDataExchange.None;
    }
}

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

unsafe static MarketDataExchange GetValue(string input)
{
    if (input.Length == 1)
        return (MarketDataExchange)(input[0]);
    fixed (char* buffer = input)
        return (MarketDataExchange)(buffer[0] << 8 | buffer[1]);
}

public enum MarketDataExchange
{
    NBBO = 0x00, //
    AMEX = 0x41, //A
    BSE = 0x42, //B
    BATS = 0x4254, //BT
    NSE = 0x43, //C
    CHX = 0x4D57, //MW
    NYSE = 0x4E, //N
    ARCA = 0x5041, //PA
    NASDAQ = 0x51, //Q
    NASDAQ_ADF = 0x5144, //QD
    CBOE = 0x57, //W
    PHLX = 0x58, //X
    DIRECTEDGE = 0x59, //Y

    None = -1
}

Ответ 14

+1 для использования словаря. Не обязательно для оптимизации, но это было бы чище.

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

Ответ 15

Беспокойный, но с использованием комбинации вложенных ifs и жесткого кодирования может просто победить оптимизатора: -

   if (ActivCode < "N") {
         // "" to "MW"
         if (ActiveCode < "BT") {
            // "" to "B"
            if (ActiveCode < "B") {
                // "" or "A"
                if (ActiveCode < "A") {
                      // must be ""
                     retrun MarketDataExchange.NBBO;
                } else {
                     // must be "A"
                    return MarketDataExchange.AMEX;
                }
            } else {
                // must be "B"
                return MarketDataExchange.BSE;
            }
         } else {
            // "BT" to "MW"
            if (ActiveCode < "MW") {
                // "BT" or "C"
                if (ActiveCode < "C") {
                      // must be "BT"
                     retrun MarketDataExchange.NBBO;
                } else {
                     // must be "C"
                    return MarketDataExchange.NSE;
                }
            } else {
            // must be "MV"
                return MarketDataExchange.CHX;
            }
         }
    } else {
        // "N" TO "Y"
         if (ActiveCode < "QD") {
            // "N" to "Q"
            if (ActiveCode < "Q") {
                // "N" or "PA"
                if (ActiveCode < "PA") {
                      // must be "N"
                     retrun MarketDataExchange.NYSE;
                } else {
                     // must be "PA"
                    return MarketDataExchange.ARCA;
                }
            } else {
                // must be "Q"
                return MarketDataExchange.NASDAQ;
            }
         } else {
            // "QD" to "Y"
            if (ActiveCode < "X") {
                // "QD" or "W"
                if (ActiveCode < "W") {
                      // must be "QD"
                     retrun MarketDataExchange.NASDAQ_ADF;
                } else {
                     // must be "W"
                    return MarketDataExchange.CBOE;
                }
            } else {
            // "X" or "Y"
                if (ActiveCode < "Y") {
                      // must be "X"
                     retrun MarketDataExchange.PHLX;
                } else {
                     // must be "Y"
                    return MarketDataExchange.DIRECTEDGE;
                }
            }
         }
    }

Это получает правильную функцию с тремя или четырьмя сравнениями. Я даже не думал бы делать это по-настоящему, если ваш фрагмент кода не будет запускаться несколько раз в секунду!

Вы также отимизируете его так, чтобы происходили только одиночные сравнения. например заменить '< "BT" 'с' >= "B" '- все это немного быстрее и даже менее читаемо!

Ответ 16

Все ваши строки имеют длину не более 2 символов и ASCII, поэтому мы можем использовать 1 байт за char. Более того, более вероятно, что они также никогда не могут иметь \0 в них (.NET string допускает внедренные нулевые символы, но многие другие вещи нет). С этим допущением мы можем набрать все ваши строки равными 2 байтам или ushort:

""   -> (byte) 0 , (byte) 0   -> (ushort)0x0000
"A"  -> (byte)'A', (byte) 0   -> (ushort)0x0041
"B"  -> (byte)'B', (byte) 0   -> (ushort)0x0042
"BT" -> (byte)'B', (byte)'T'  -> (ushort)0x5442

Теперь, когда у нас есть единственное целое число в относительно коротком диапазоне (64 КБ), мы можем использовать таблицу поиска:

MarketDataExchange[] lookup = {
    MarketDataExchange.NBBO, 
    MarketDataExchange.NONE, 
    MarketDataExchange.NONE, 
    ...
    /* at index 0x041 */
    MarketDataExchange.AMEX,
    MarketDataExchange.BSE,
    MarketDataExchange.NSE,
    ...
};

Теперь получение значения, заданного строкой, следующее:

public static unsafe MarketDataExchange GetMarketDataExchange(string s)
{
   // Assume valid input
   if (s.Length == 0) return MarketDataExchange.NBBO;

   // .NET strings always have '\0' after end of data - abuse that
   // to avoid extra checks for 1-char strings. Skip index checks as well.
   ushort hash;
   fixed (char* data = s)
   {
       hash = (ushort)data[0] | ((ushort)data[1] << 8);
   }

   return lookup[hash];
}

Ответ 17

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

Ответ 18

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

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

Ответ 19

Несколько случайных мыслей, которые могут быть не все применимы вместе:

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

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

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

И если вам действительно нужно, чтобы он был как можно быстрее, почему бы вам не написать его в сборке?:)

Ответ 20

Можно ли включить ActivCode в int, а затем использовать int в наших операциях case?

Ответ 21

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

public static MarketDataExchange GetMarketDataExchange(string ActivCode)
{
    if (ActivCode == null)
        return MarketDataExchange.NONE;
    if (ActivCode.Length == 0)
        return MarketDataExchange.NBBO;
    return (MarketDataExchange)((ActivCode[0] << ActivCode.Length));
}

public enum MarketDataExchange
{
    NONE = 0,
    NBBO = 1,
    AMEX = ('A'<<1),
    BSE = ('B'<<1),
    BATS = ('B'<<2),
    NSE = ('C'<<1),
    CHX = ('M'<<2),
    NYSE = ('N'<<1),
    ARCA = ('P'<<2),
    NASDAQ = ('Q'<<1),
    NASDAQ_ADF = ('Q'<<2),
    CBOE = ('W'<<1),
    PHLX = ('X'<<1),
    DIRECTEDGE = ('Y'<<1),
}