Прежде чем проголосовать против или начать говорить, что goto
ing является злом и устаревшим, пожалуйста, прочтите обоснование того, почему он жизнеспособен в данном случае. Прежде чем пометить его как дубликат, прочтите вопрос полностью.
Я читал об интерпретаторах виртуальных машин, когда я наткнулся на вычисляемые переходы. Очевидно, они позволяют значительно улучшить производительность определенных фрагментов кода. Самый известный пример - это основной цикл интерпретатора виртуальной машины.
Рассмотрим (очень) простую виртуальную машину, подобную этой:
#include <iostream>
enum class Opcode
{
HALT,
INC,
DEC,
BIT_LEFT,
BIT_RIGHT,
RET
};
int main()
{
Opcode program[] = { // an example program that returns 10
Opcode::INC,
Opcode::BIT_LEFT,
Opcode::BIT_LEFT,
Opcode::BIT_LEFT,
Opcode::INC,
Opcode::INC,
Opcode::RET
};
int result = 0;
for (Opcode instruction : program)
{
switch (instruction)
{
case Opcode::HALT:
break;
case Opcode::INC:
++result;
break;
case Opcode::DEC:
--result;
break;
case Opcode::BIT_LEFT:
result <<= 1;
break;
case Opcode::BIT_RIGHT:
result >>= 1;
break;
case Opcode::RET:
std::cout << result;
return 0;
}
}
}
Все, что может сделать эта виртуальная машина, - это выполнить несколько простых операций с одним числом типа int
и распечатать его. Несмотря на сомнительную полезность, он тем не менее иллюстрирует предмет.
Критической частью виртуальной машины, очевидно, является оператор switch
в цикле for
. Его производительность определяется многими факторами, наиболее важными из которых, безусловно, являются прогноз ветвления и действие перехода к соответствующей точке выполнения (метки case
).
Здесь есть место для оптимизации. Чтобы ускорить выполнение этого цикла, можно использовать так называемые вычисляемые переходы.
Вычисленные Gotos
Вычисляемые gotos - это конструкция, хорошо известная программистам на Фортране и тем, кто использует определенное (нестандартное) расширение GCC. Я не поддерживаю использование какого-либо нестандартного, определяемого реализацией и (очевидно) неопределенного поведения. Однако, чтобы проиллюстрировать рассматриваемую концепцию, я буду использовать синтаксис упомянутого расширения GCC.
В стандартном C ++ нам разрешено определять метки, к которым впоследствии можно будет перейти с помощью оператора goto
:
goto some_label;
some_label:
do_something();
Это не считается хорошим кодом (и по уважительной причине!) . Хотя есть веские аргументы против использования goto
(большинство из которых связано с ремонтопригодностью кода), есть применение для этой отвратительной функции. Это улучшение производительности.
Чтобы использовать весь потенциал goto
, было сделано расширение компилятора GCC, которое позволяет goto
быть более динамичным. То есть метку, к которой нужно перейти, можно определить во время выполнения.
Это расширение позволяет получить указатель метки, аналогичный указателю на функцию, и goto
к нему:
void* label_ptr = &&some_label;
goto (*label_ptr);
some_label:
do_something();
Это интересная концепция, которая позволяет нам еще больше улучшить нашу простую виртуальную машину. Вместо использования оператора switch
мы будем использовать массив указателей меток (так называемую таблицу переходов), а затем goto
на соответствующий (код операции будет использоваться для индексации массива):
// [Courtesy of Eli Bendersky][4]
// This code is licensed with the [Unlicense][5]
int interp_cgoto(unsigned char* code, int initval) {
/* The indices of labels in the dispatch_table are the relevant opcodes
*/
static void* dispatch_table[] = {
&&do_halt, &&do_inc, &&do_dec, &&do_mul2,
&&do_div2, &&do_add7, &&do_neg};
#define DISPATCH() goto *dispatch_table[code[pc++]]
int pc = 0;
int val = initval;
DISPATCH();
while (1) {
do_halt:
return val;
do_inc:
val++;
DISPATCH();
do_dec:
val--;
DISPATCH();
do_mul2:
val *= 2;
DISPATCH();
do_div2:
val /= 2;
DISPATCH();
do_add7:
val += 7;
DISPATCH();
do_neg:
val = -val;
DISPATCH();
}
}
Эта версия примерно на 25% быстрее, чем та, в которой используется switch
(тот, что указан в сообщении блога по ссылке, а не тот, что выше). Это связано с тем, что после каждой операции выполняется только один переход вместо двух.
Поток управления с switch
: Например, если бы мы хотели выполнить
Opcode::FOO
, а затем Opcode::SOMETHING
, это выглядело бы так: Как видите, после выполнения инструкции выполняется два перехода. Первый - это возврат к коду
switch
, а второй - к фактической инструкции.
Напротив, если бы мы использовали массив указателей меток (напомним, они нестандартные), у нас был бы только один переход:
Стоит отметить, что помимо экономии циклов за счет выполнения меньшего количества операций, мы также улучшаем качество предсказания ветвлений, устраняя дополнительный переход.
Теперь мы знаем, что, используя массив указателей меток вместо switch
, мы можем значительно улучшить производительность нашей виртуальной машины (примерно на 20%). Я подумал, что, возможно, для этого могут быть и другие приложения.
Я пришел к выводу, что эту технику можно использовать в любой программе, имеющей цикл, в котором он последовательно косвенно отправляет некоторую логику. Простым примером этого (помимо виртуальной машины) может быть вызов метода virtual
для каждого элемента контейнера полиморфных объектов:
std::vector<Base*> objects;
objects = get_objects();
for (auto object : objects)
{
object->foo();
}
Теперь у этого гораздо больше приложений.
Однако есть одна проблема: в стандартном C ++ нет ничего, кроме указателей меток. Таким образом, возникает вопрос: есть ли способ имитировать поведение вычисленных goto
в стандартном C ++ , которое могло бы соответствовать им по производительности?.
Изменить 1:
Есть еще одна обратная сторона использования переключателя. Мне об этом напомнил user1937198. Это обязательная проверка. Короче говоря, он проверяет, соответствует ли значение переменной внутри switch
любому из case
. Он добавляет избыточное ветвление (эта проверка предусмотрена стандартом).
Изменить 2:
В ответ на сообщение командира, я поясню, в чем моя идея по сокращению накладных расходов на вызовы виртуальных функций. Грязный подход к этому заключался бы в наличии идентификатора в каждом производном экземпляре, представляющего его тип, который будет использоваться для индексации таблицы переходов (массива указателей меток). Проблема в том, что:
- Таблицы переходов отсутствуют - стандартный C ++
- При добавлении нового производного класса потребуется изменить все таблицы переходов.
Я был бы благодарен, если бы кто-то придумал какой-то тип магии шаблонов (или макрос в крайнем случае), который позволил бы написать его, чтобы он был более чистым, расширяемым и автоматизированным, например:
switch
es реализованы в терминахgoto
внизу, поэтому для меня это не имеет смысла. Но я не могу это проверить. И это единственное, что я могу дать этому разговору. - person   schedule 09.11.2019switch
es по сутиgoto
s. Дело в том, что на самом деле переключатель состоит из двухgoto
, как вы можете видеть на схеме. - person   schedule 09.11.2019