Таблица поиска и переключатель во встроенном программном обеспечении C

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

Итак, я хотел бы понять различия между этим:

Справочная таблица

static void func1(){}
static void func2(){}

typedef enum
{
    FUNC1,
    FUNC2,
    FUNC_COUNT
} state_e;

typedef void (*func_t)(void);

const func_t lookUpTable[FUNC_COUNT] =
{
    [FUNC1] = &func1,
    [FUNC2] = &func2
};

void fsm(state_e state)
{
    if (state < FUNC_COUNT) 
        lookUpTable[state]();
    else
        ;// Error handling
}

и это:

Выключатель

static void func1(){}
static void func2(){}

void fsm(int state)
{
    switch(state)
    {
        case FUNC1: func1(); break;
        case FUNC2: func2(); break;
        default:    ;// Error handling
    }
}

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

Спасибо за вашу помощь!


person Plouff    schedule 07.03.2016    source источник
comment
Мы не можем дать вам ответ на этот вопрос, так как он зависит от слишком многих вещей, но в основном от используемого вами компилятора. Вместо этого вы должны указать своему компилятору выводить сборку в обоих случаях, используя флаги оптимизации, и сравнить ее самостоятельно.   -  person nos    schedule 07.03.2016
comment
Обратите внимание на следующий пост об операторах switch: lazarenko.me/switch   -  person Guillaume George    schedule 07.03.2016
comment
@nos: На самом деле это моя точка зрения. Я имею в виду, я думал, что независимо от компилятора, переключение будет медленнее! В своем первоначальном вопросе я забыл сказать, что моя переменная состояния является непрерывным значением (это перечисление). Я обновил свой вопрос, чтобы добавить это.   -  person Plouff    schedule 07.03.2016
comment
@GuillaumeGeorge: Спасибо за ссылку. Когда я ее прочитаю, надеюсь, она даст мне новый взгляд на проблему!   -  person Plouff    schedule 07.03.2016
comment
Некоторые компиляторы преобразуют простые операторы switch в таблицы поиска. Общий ответ действительно невозможен.   -  person Sebastian Mach    schedule 07.03.2016
comment
@phresnel: Я не мог сформулировать это так вначале, но я думал об аппаратном механизме. Ответы ниже помогли мне понять, почему нет эмпирического правила. Но опять же, я действительно мог бы выразить это так!   -  person Plouff    schedule 07.03.2016
comment
@phresnel: это практика › 20 лет в компиляторах. Тем не менее, есть различия, например. где находится таблица и как к ней осуществляется доступ (инструкция или выборка данных). Это очень важные отличия для встраиваемых систем (см. мой ответ). Он сильно отличается от ПК-подобных систем (которые включают в себя большинство систем на базе ARM Cortex-A).   -  person too honest for this site    schedule 07.03.2016
comment
@Olaf: Не знаю, почему вы учите меня, что это практикуется уже 20 лет, тем более что это не всегда верно и не происходит для каждого коммутатора и со всеми флагами оптимизации. Это также зависит от эвристики затрат/выгод. Не каждая LUT является оптимизацией, как и не каждая жестко запрограммированная структура if-else.   -  person Sebastian Mach    schedule 08.03.2016
comment
@phresnel: я не говорил, что это делается для каждого switch; конечно, это зависит от этикеток. Может быть, я должен был использовать обычную практику или современное состояние, но это то, что есть. В любом случае, если ваш компилятор не делает этого для простого switch с увеличением (на одну) меток, вам следует взять современный. До сих пор существуют дорогие мусорные компиляторы, особенно в области встраиваемых систем. Во всяком случае, это не было целью моего комментария!   -  person too honest for this site    schedule 08.03.2016
comment
@Olaf: Хотя существуют и другие оптимизации. Я не уверен в разреженных таблицах поиска, но были замечены бинарные деревья. Например, programming.sirrida.de/hashsuper.pdf (вы, наверное, видели это). В любом случае, слишком долго для комментариев :)   -  person Sebastian Mach    schedule 08.03.2016
comment
@phresnel: ... и все еще не понял сути.   -  person too honest for this site    schedule 08.03.2016
comment
Многие компиляторы не могут встроить вызов указателя функции (или могут потребовать несколько конкретных параметров реализации) и, таким образом, упускают любые оптимизации, которые сопровождают встраивание... просто кое-что, о чем следует помнить.   -  person technosaurus    schedule 10.03.2016
comment
Переключатели или обычно исполняемый код по таблицам поиска должны работать быстрее. В документе, посвященном генератору сканера re2c, представлены некоторые эмпирические исследования этого re2c.org/_downloads/.   -  person PSkocik    schedule 17.10.2016


Ответы (6)


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

Такие встроенные системы обычно имеют следующие ограничения.

  • нет кеша процессора.
  • Flash требует состояний ожидания для более высоких (т.е.> 32 МГц) тактовых частот ЦП. Фактическое соотношение зависит от конструкции кристалла, маломощного/высокоскоростного процесса, рабочего напряжения и т. д.
  • Чтобы скрыть состояния ожидания, Flash имеет более широкие строки чтения, чем шина ЦП.
  • Это хорошо работает только для линейного кода с предварительной выборкой инструкций.
  • Доступы к данным нарушают предварительную выборку инструкций или приостанавливаются до ее завершения.
  • Flash может иметь внутренний очень маленький кэш инструкций.
  • Если вообще есть, то кэш данных еще меньше.
  • Небольшие кэши приводят к более частому удалению (замене предыдущей записи до того, как она использовалась в другой раз).

Например, STM32F4xx чтение занимает 6 тактов на частоте 150 МГц/3,3 В для 128 бит (4 слова). Таким образом, если требуется доступ к данным, велика вероятность, что это добавит задержку более 12 часов для всех данных, которые будут извлечены (есть дополнительные циклы).

Предполагая компактные коды состояний, для реальной проблемы это имеет следующие последствия для этой архитектуры (Cortex-M4):

  • Таблица поиска: Чтение адреса функции является доступом к данным. Со всеми последствиями, указанными выше.
  • Переключатель otoh использует специальную инструкцию «поиска в таблице», которая использует данные кодового пространства сразу после инструкции. Таким образом, первые записи, возможно, уже предварительно загружены. Другие записи не нарушают предварительную выборку. Кроме того, доступ является кодовым доступом, поэтому данные попадают в кэш инструкций Flash.

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


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

person too honest for this site    schedule 07.03.2016
comment
Ваш ответ действительно более актуален, поскольку я говорил о встроенном программном обеспечении. Моя цель не STM32, а MCU. Мне требуется 8 циклов, чтобы добиться чтения на FLASH. К сожалению, я предпочитаю использовать функции, даже если бы я переделал код в switch. Поэтому я также должен учитывать читабельность решения. Помимо точки зрения эффективности, я предпочитаю удобочитаемость таблицы поиска. Но это дело вкуса! Спасибо за очень подробный ответ (знак ответа поставил :)! - person Plouff; 07.03.2016
comment
Дополнительный вопрос: такое поведение не имеет прямого отношения к сборке, верно? Я имею в виду, является ли инструментирование кода (например, переключение GPIO) единственным решением, позволяющим увидеть эффекты этого механизма hw? Спасибо! - person Plouff; 07.03.2016
comment
@Plouff: я не уверен, что вы имеете в виду под последним комментарием. Это, безусловно, деталь сборки/реализации, как всегда, когда речь идет о производительности. Надеюсь, я ясно дал понять, что здесь задействовано множество факторов. Об использовании функций: Если вы используете современный компилятор (например, gcc), объявите функции static, он вполне может встроить их в switch (также зависит от настроек оптимизации). Добавление inline может дать компилятору еще более сильный намек (но не обязательно). Не уверен, как ведут себя более консервативные компиляторы, такие как IAR (иногда они склонны хуже оптимизировать такие конструкции). - person too honest for this site; 07.03.2016
comment
Я имею в виду, что я не понимаю, как я мог увидеть эффект состояния ожидания флеша в сборке. Это правильное предположение? Я читал материал о функции inline в прошлом. Но я помню, что это не означает, что компилятор на самом деле встроит функцию. Более того, inline — это функция C99. По всем этим причинам я не использую его много... Может быть, пришло время попробовать. Спасибо за подсказку! Функция @имя сломана?!) - person Plouff; 07.03.2016
comment
inline — это стандартная функция C, начиная с C99, текущая версия — C11. Существует только один стандарт C, поэтому, если говорить о C, это C11. C99 обратно совместим; различия здесь не имеют значения. Вы не должны использовать устаревший компилятор, который не поддерживает как минимум C99 — он уже 5 лет вытеснен C11 и 17 лет с момента его выпуска! Пожалуйста, внимательно прочитайте мой комментарий. Я писал о современных компиляторах, а не о каком-то хламе — но дорогом — проприетарном тулчейне. Если бы вы указали, какой MCU вы используете, я мог бы дать еще несколько советов. - person too honest for this site; 07.03.2016
comment
На данный момент я нахожусь на цели TI C2000. Но, не называя их, бывает, что мне приходится работать со старыми микроконтроллерами, набор инструментов которых поддерживает только C89!! (Думаю, я не могу @name вас, так как вы единственный, кто комментирует здесь!) - person Plouff; 08.03.2016
comment
@Plouff: Я помню, как в университетской лаборатории в 90-х годах мы использовали микроконтроллер TI (не уверен, какой), где мы действительно видели такие оптимизации. Только что проверил. C2000 Flash действительно имеет состояния ожидания. Однако не уверен насчет кешей и т. Д. Я бы проверил, еслиf это действительно проблема. - person too honest for this site; 09.03.2016
comment
Да, у C2000 есть состояния ожидания (8 в моем приложении). Кроме того, у них нет кеша, а есть буфер предварительной выборки. В любом случае, как вы сказали, проблем нет на данный момент, поэтому мне не нужно проводить тест на данный момент. но мы планируем добавлять все больше и больше функций, поэтому позже это может стать проблемой. И я предпочитаю предвидеть эти проблемы с потенциальными решениями. Еще раз спасибо - person Plouff; 09.03.2016

Во-первых, на некоторых процессорах непрямые вызовы (например, через указатель) — как в вашем примере Lookup Table — обходятся дорого (разрыв конвейера, TLB, эффекты кеша). Это также может быть верно для непрямых переходов...

Затем хороший оптимизирующий компилятор может встроить вызов func1() в ваш пример Switch; тогда вы не будете запускать пролог или эпилог для встроенных функций.

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

person Basile Starynkevitch    schedule 07.03.2016
comment
Спасибо именно тот ответ, который я искал! Я понятия не имел, что непрямые вызовы приведут к таким вещам, как разрыв конвейера, TLB и эффекты кеша. Но теперь мне нужно понять, что это значит... Сначала у меня был один вопрос, а теперь еще 3! Спасибо ;)! - person Plouff; 07.03.2016
comment
@BarryTheHatchet: я говорю об этом: stackoverflow.com/q/35797254/882697 . Я не думаю, что вы комментировали здесь. О какой нити вы говорите? Это может быть интересно для меня :). - person Plouff; 07.03.2016
comment
@Plouff Хорошо, должно быть, тогда был еще один пост. Стечение обстоятельств! - person Lightness Races in Orbit; 07.03.2016

Использование LUT указателей на функции заставляет компилятор использовать эту стратегию. Теоретически он может скомпилировать версию коммутатора, по сути, с тем же кодом, что и версия LUT (теперь, когда вы добавили проверки выхода за пределы к обеим). На практике это не то, что делают gcc или clang, поэтому стоит посмотреть на вывод asm, чтобы увидеть, что произошло.

(обновление: gcc -fpie (включен по умолчанию в большинстве современных дистрибутивов Linux) любит создавать таблицы относительных смещений вместо абсолютных указателей функций, поэтому родата также не зависит от позиции. Код инициализации таблицы переходов GCC, генерирующий movsxd и add?. Это может быть пропущенный- оптимизация, см. мой ответ там для ссылок на отчеты об ошибках gcc, Ручное создание массива указателей на функции может обойти это.)


Я поставил код <сильный> в проводнике компилятора Godbolt с обеими функциями в одном блоке компиляции (с выводом gcc и clang), чтобы увидеть, как он на самом деле компилируется. Я немного расширил функции, чтобы это было не просто два случая.

void fsm_switch(int state) {
    switch(state) {
        case FUNC0: func0(); break;
        case FUNC1: func1(); break;
        case FUNC2: func2(); break;
        case FUNC3: func3(); break;
        default:    ;// Error handling
    }
    //prevent_tailcall();
}

void fsm_lut(state_e state) {
    if (likely(state < FUNC_COUNT))  // without likely(), gcc puts the LUT on the taken side of this branch
        lookUpTable[state]();
    else
        ;// Error handling
    //prevent_tailcall();
}

См. также Как в ядре Linux работают макросы вероятного() и маловероятного() и в чем их польза?


x86

В x86 clang создает собственный LUT для переключателя, но записи являются указателями внутри функции, а не конечными указателями функций. Таким образом, для clang-3.7 переключатель компилируется в код, который явно хуже, чем LUT, реализованный вручную. В любом случае, процессоры x86, как правило, имеют предсказание ветвлений, которое может обрабатывать косвенные вызовы/переходы, по крайней мере, если их легко предсказать.

GCC использует последовательность условных переходов (но, к сожалению, не 't tail-call напрямую с условными ветвями, что AFAICT безопасно на x86. Он проверяет 1, ‹1, 2, 3, в этом порядке, с в основном неиспользованными ветвями, пока не найдет совпадение.

Они делают практически идентичный код для LUT: проверка границ, обнуление старших 32-бит регистра arg с помощью mov, а затем косвенный переход к памяти с режимом индексированной адресации.


РУКА:

gcc 4.8.2 с -mcpu=cortex-m4 -O2 делает интересный код.

Как сказал Олаф, он создает встроенную таблицу из 1 миллиарда записей. Он не переходит непосредственно к целевой функции, а вместо этого переходит к обычной инструкции перехода (например, b func3). Это обычный безусловный прыжок, так как это хвостовой вызов.

Каждый нуждается запись в таблице назначения значительно больше кода (Godbolt), если fsm_switch делает что-либо после вызова (например, в этом случае вызов не встроенной функции, если void prevent_tailcall(void); объявлен, но не определен) или если это встроено в более крупную функцию.

@@ With   void prevent_tailcall(void){} defined so it can inline:
@@ Unlike in the godbolt link, this is doing tailcalls.
fsm_switch:
        cmp     r0, #3    @ state,
        bhi     .L5       @
        tbb     [pc, r0]  @ state
       @@ There's no section .rodata directive here: the table is in-line with the code, so there's no need for base pointer to be loaded into a reg.  And apparently it's even loaded from I-cache, not D-cache
        .byte   (.L7-.L8)/2
        .byte   (.L9-.L8)/2
        .byte   (.L10-.L8)/2
        .byte   (.L11-.L8)/2
.L11:
        b       func3     @ optimized tail-call
.L10:
        b       func2
.L9:
        b       func1
.L7:
        b       func0
.L5:
        bx      lr         @ This is ARM's equivalent of an x86 ret insn

IDK, если есть большая разница между тем, насколько хорошо работает прогнозирование ветвлений для tbb, и полным непрямым переходом или вызовом (blx) на облегченном ядре ARM. Доступ к данным для загрузки таблицы может быть более важным, чем двухшаговый переход к инструкции ветвления, которую вы получаете с помощью switch.

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

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

Функция LUT должна потратить пару инструкций на загрузку базового адреса LUT в регистр, но в остальном она очень похожа на x86:

fsm_lut:
        cmp     r0, #3    @ state,
        bhi     .L13      @,
        movw    r3, #:lower16:.LANCHOR0 @ tmp112,
        movt    r3, #:upper16:.LANCHOR0 @ tmp112,
        ldr     r3, [r3, r0, lsl #2]      @ tmp113, lookUpTable
        bx      r3  @ indirect register sibling call    @ tmp113
.L13:
        bx      lr  @

@ in the .rodata section
lookUpTable:
        .word   func0
        .word   func1
        .word   func2
        .word   func3

См. Mike of SST answer для аналогичного анализ на Microchip dsPIC.

person Peter Cordes    schedule 13.03.2016
comment
Это еще один отличный ответ, большое спасибо! Мне стало интересно, все ли видно при сборке. Теперь я знаю, что вы должны знать, как ваше оборудование справится со сборкой, которую вы ему подадите! Итак, вы отвечаете на другой вопрос, который у меня был: ответ на этот вопрос зависит не только от используемого вами компилятора, но и от оборудования. Таким образом, единственным полностью эффективным решением является бенчмаркинг, а не анализ кода (если только вы не знаете абсолютно всех механизмов hw). Еще раз спасибо! - person Plouff; 15.03.2016
comment
Я хотел бы снова +1 вам за открытие вероятного () и высокие эффекты вызова! - person Plouff; 15.03.2016
comment
@Plouff: на самом деле, глядя на asm и зная, что медленно работает в ряде аппаратных семейств, можно как бы заменить тесты. У большинства людей нет ни одного из всех x86 uarch, лежащих в тестовой ферме. Хотя для встраиваемых систем вы, конечно, можете просто протестировать свою целевую платформу. - person Peter Cordes; 16.03.2016
comment
Да, именно это я и пытался сказать :). В моем случае бенчмаркинг будет единственным решением, если только я не потрачу много времени на изучение архитектуры моей цели! - person Plouff; 16.03.2016
comment
На многих платформах, основанных на хранилище кода медленной флэш-памяти, ручное использование таблицы позволит контролировать, хранится ли таблица в быстрой ОЗУ или медленной флэш-памяти. Я не думаю, что когда-либо видел компилятор с опциями для управления генерацией кода оператора switch таким образом. - person supercat; 07.09.2018

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

Однако обратите внимание, что ваши 2 фрагмента кода не выполняют одинаковую проверку на state:

  • Переключатель изящно ничего не сделает, если state не является одним из определенных значений,
  • Версия с таблицей переходов вызовет неопределенное поведение для всех, кроме двух значений FUNC1 и FUNC2.

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

void fsm(int state) {
    if (state >= 0 && state < FUNC_COUNT && lookUpTable[state] != NULL)
        lookUpTable[state]();
}

Попробуйте протестировать это и проверить ассемблерный код. Вот удобный онлайн-компилятор для этого: http://gcc.godbolt.org/#

person chqrlie    schedule 07.03.2016
comment
В этом вопросе я хотел сосредоточиться на таблице поиска и коммутаторе. Но вы правы: государственная проверка тоже имеет свою цену! Я добавил некоторые детали, чтобы лучше отразить реализацию, которую я использую. Пока что интересно заметить, что в 3 ответах, которые у меня были, всегда один и тот же совет: тест! Спасибо за онлайн-компилятор, это будет очень полезная ссылка! - person Plouff; 07.03.2016
comment
В правильной реализации (т. е. включая перехват недопустимых состояний и компактный переключатель) оба приведут к очень похожей конструкции. Подробности больше в аппаратной архитектуре, как данные считываются из Flash, типах доступа и т.д. - person too honest for this site; 07.03.2016

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

Например, на dsPIC33E512MU810 при использовании XC16 (v1.24) код поиска:

lookUpTable[state]();

Компилируется в (из окна дизассемблирования в MPLAB-X):

!        lookUpTable[state]();
0x2D20: MOV [W14], W4    ; get state from stack-frame (not counted)
0x2D22: ADD W4, W4, W5   ; 1 cycle (addresses are 16 bit aligned)
0x2D24: MOV #0xA238, W4  ; 1 cycle (get base address of look-up table)
0x2D26: ADD W5, W4, W4   ; 1 cycle (get address of entry in table)
0x2D28: MOV [W4], W4     ; 1 cycle (get address of the function)
0x2D2A: CALL W4          ; 2 cycles (push PC+2 set PC=W4)

... и каждая (пустая, ничего не делающая) функция компилируется в:

!static void func1()
!{}
0x2D0A: LNK #0x0         ; 1 cycle (set up stack frame)
! Function body goes here
0x2D0C: ULNK             ; 1 cycle (un-link frame pointer)
0x2D0E: RETURN           ; 3 cycles

Это в общей сложности 11 командных циклов накладных расходов для любого из случаев, и все они занимают одно и то же место. (Примечание. Если таблица или содержащиеся в ней функции не находятся на одной и той же флэш-странице размером 32 КБ программного слова, возникнут еще большие накладные расходы из-за необходимости заставить блок генерации адресов читать с правильной страницы или настроить ПК, чтобы сделать длинный вызов.)

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

Например, оператор switch:

switch(state)
{
case FUNC1: state++; break;
case FUNC2: state--; break;
default: break;
}

Компилируется в:

!    switch(state)
0x2D2C: MOV [W14], W4       ; get state from stack-frame (not counted)
0x2D2E: SUB W4, #0x0, [W15] ; 1 cycle (compare with first case)
0x2D30: BRA Z, 0x2D38       ; 1 cycle (if branch not taken, or 2 if it is)
0x2D32: SUB W4, #0x1, [W15] ; 1 cycle (compare with second case)
0x2D34: BRA Z, 0x2D3C       ; 1 cycle (if branch not taken, or 2 if it is)
!    {
!    case FUNC1: state++; break;
0x2D38: INC [W14], [W14]    ; To stop the switch being optimised out
0x2D3A: BRA 0x2D40          ; 2 cycles (go to end of switch)
!    case FUNC2: state--; break;
0x2D3C: DEC [W14], [W14]    ; To stop the switch being optimised out
0x2D3E: NOP                 ; compiler did a fall-through (for some reason)
!    default: break;
0x2D36: BRA 0x2D40          ; 2 cycles (go to end of switch)
!    }

Это накладные расходы в 5 циклов, если берется первый случай, 7, если берется второй случай, и т. д., что означает, что они безубыточны в четвертом случае.

Это означает, что знание ваших данных во время разработки окажет значительное влияние на скорость в долгосрочной перспективе. Если у вас есть значительное число (более 4 случаев) и все они происходят с одинаковой частотой, то в долгосрочной перспективе справочная таблица будет быстрее. Если частота случаев значительно отличается (например, случай 1 более вероятен, чем случай 2, который более вероятен, чем случай 3 и т. д.), то, если вы сначала заказываете переключатель с наиболее вероятным случаем, тогда переключатель будет быстрее в долгосрочной перспективе. Для крайнего случая, когда у вас есть только несколько случаев, переключатель (вероятно) будет быстрее в любом случае для большинства выполнений и будет более читабельным и менее подверженным ошибкам.

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

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

Изменить: мой пример с переключением немного несправедлив, так как я проигнорировал первоначальный вопрос и вставил «основу» обращений, чтобы подчеркнуть реальное преимущество использования переключателя над поиском. . Если коммутатор также должен выполнять вызов, то он имеет преимущество только в первом случае!

person Evil Dog Pie    schedule 07.03.2016
comment
Спасибо за этот кейс. На данный момент у меня около 20 состояний (т.е. кейсов). В будущем может быть больше. Единственная проблема, которую я вижу в вашем ответе, заключается в том, что переключатель не преобразуется компилятором в таблицу переходов. Это было бы чудесно. Более того, как вы сказали, у вас нет накладных расходов на вызов подпрограммы в switch, но примеры все равно дают хорошее представление. Еще раз спасибо! - person Plouff; 08.03.2016
comment
@Plouff Вы правы, компилятор Microchip не переводит операторы switch в таблицу переходов просто потому, что тест с двумя инструкциями и последовательность ветвлений более эффективны. Это дает разработчику выбор решений в зависимости от его требований. - person Evil Dog Pie; 08.03.2016
comment
@Plouff Я решил игнорировать вызовы в случаях, потому что ваш пример кода подразумевает, что вариант использования является конечным автоматом. В таких случаях я обычно стараюсь поддерживать обработку перехода между состояниями встроенной в случаи оператора switch (чтобы управление состоянием было инкапсулировано) и при необходимости делегировать конкретную обработку перехода между состояниями другим функциям. Это также позволяет коду использовать (небольшой) прирост производительности коммутатора по сравнению с таблицей переходов, где это возможно. Это все очень субъективно, и я не сомневаюсь, что у вашего проекта другие требования, чем у меня. :-) - person Evil Dog Pie; 08.03.2016
comment
Спасибо за подробности. Вы бы сказали, что около 20 штатов — это много штатов? - person Plouff; 08.03.2016
comment
Как правило, да. Но если одно или два состояния возникают значительно чаще, чем другие, я ожидаю, что переключение будет более эффективным. Однако выбор между таблицей переходов и переключателем во многом зависит от компилятора и вашего целевого процессора. Обычно вы можете заставить компилятор выводить «промежуточный» файл, содержащий скомпилированные инструкции на языке ассемблера. Было бы неплохо взглянуть на это и сравнить их. Но если вы не знаете наверняка, что это требует оптимизации, ваше время, вероятно, лучше потратить на другие вещи, так как разница между ними очень мала. - person Evil Dog Pie; 09.03.2016
comment
Хорошо, спасибо за ваш отзыв :). Как я уже где-то сказал, я сейчас не занимаюсь такой оптимизацией. Но он мне может понадобиться в будущем. - person Plouff; 09.03.2016

Чтобы получить еще больше выходных данных компилятора, вот что создается компилятором TI C28x с использованием примера кода @PeterCordes:

_fsm_switch:
        CMPB      AL,#0                 ; [CPU_] |62| 
        BF        $C$L3,EQ              ; [CPU_] |62| 
        ; branchcc occurs ; [] |62| 
        CMPB      AL,#1                 ; [CPU_] |62| 
        BF        $C$L2,EQ              ; [CPU_] |62| 
        ; branchcc occurs ; [] |62| 
        CMPB      AL,#2                 ; [CPU_] |62| 
        BF        $C$L1,EQ              ; [CPU_] |62| 
        ; branchcc occurs ; [] |62| 
        CMPB      AL,#3                 ; [CPU_] |62| 
        BF        $C$L4,NEQ             ; [CPU_] |62| 
        ; branchcc occurs ; [] |62| 
        LCR       #_func3               ; [CPU_] |66| 
        ; call occurs [#_func3] ; [] |66| 
        B         $C$L4,UNC             ; [CPU_] |66| 
        ; branch occurs ; [] |66| 
$C$L1:    
        LCR       #_func2               ; [CPU_] |65| 
        ; call occurs [#_func2] ; [] |65| 
        B         $C$L4,UNC             ; [CPU_] |65| 
        ; branch occurs ; [] |65| 
$C$L2:    
        LCR       #_func1               ; [CPU_] |64| 
        ; call occurs [#_func1] ; [] |64| 
        B         $C$L4,UNC             ; [CPU_] |64| 
        ; branch occurs ; [] |64| 
$C$L3:    
        LCR       #_func0               ; [CPU_] |63| 
        ; call occurs [#_func0] ; [] |63| 
$C$L4:    
        LCR       #_prevent_tailcall    ; [CPU_] |69| 
        ; call occurs [#_prevent_tailcall] ; [] |69| 
        LRETR     ; [CPU_] 
        ; return occurs ; [] 



_fsm_lut:
;* AL    assigned to _state
        CMPB      AL,#4                 ; [CPU_] |84| 
        BF        $C$L5,HIS             ; [CPU_] |84| 
        ; branchcc occurs ; [] |84| 
        CLRC      SXM                   ; [CPU_] 
        MOVL      XAR4,#_lookUpTable    ; [CPU_U] |85| 
        MOV       ACC,AL << 1           ; [CPU_] |85| 
        ADDL      XAR4,ACC              ; [CPU_] |85| 
        MOVL      XAR7,*+XAR4[0]        ; [CPU_] |85| 
        LCR       *XAR7                 ; [CPU_] |85| 
        ; call occurs [XAR7] ; [] |85| 
$C$L5:    
        LCR       #_prevent_tailcall    ; [CPU_] |88| 
        ; call occurs [#_prevent_tailcall] ; [] |88| 
        LRETR     ; [CPU_] 
        ; return occurs ; [] 

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

person Plouff    schedule 15.03.2016