Прежде чем вы проголосуете против или начнете говорить, что goto
злое и устарело, пожалуйста, прочитайте обоснование того, почему оно жизнеспособно в этом случае. Прежде чем пометить его как дубликат, прочитайте полный вопрос.
Я читал о интерпретаторах виртуальных машин, когда Я наткнулся на вычисленных переходов . По-видимому, они позволяют значительно повысить производительность определенных фрагментов кода. Наиболее известным примером является основной цикл интерпретатора VM.
Рассмотрим (очень) простую виртуальную машину, подобную этой:
#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
).
Здесь есть место для оптимизации. Чтобы ускорить выполнение этого цикла, можно использовать так называемые compoted gotos .
Computed Gotos
Computed gotos - конструкция, хорошо известная Fortran. программисты и те, кто использует определенное (нестандартное) расширение GCC. Я не поддерживаю использование какого-либо нестандартного, определенного реализацией и (очевидно) неопределенного поведения. Однако для иллюстрации рассматриваемой концепции я буду использовать синтаксис упомянутого расширения GCC.
В стандарте C ++ нам разрешено определять метки, к которым позже можно перейти с помощью оператора goto
:
goto some_label;
some_label:
do_something();
Это не считается хорошим кодом ( и по уважительной причине! ). Хотя существуют веские аргументы против использования goto
(большинство из которых связаны с удобством сопровождения кода), существует приложение для этой отвратительной функции. Это повышение производительности.
Использование оператора goto
может быть быстрее, чем вызов функции. Это связано с тем, что количество «бумажной работы», такой как настройка стека и возвратзначение, которое должно быть сделано при вызове функции. Между тем goto
иногда можно преобразовать в одну jmp
инструкцию по сборке.
Чтобы использовать весь потенциал 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
s в стандартном C ++ , которое может соответствовать им по производительности? .
Edit 1:
Существует еще один недостаток использования переключателя. Мне напомнили об этом user1937198 . Это обязательная проверка. Короче говоря, он проверяет, совпадает ли значение переменной внутри switch
с любым из case
s. Он добавляет избыточное ветвление (эта проверка обязательна по стандарту).
Редактировать 2:
В ответ на cmaster я поясню, что я имею в виду по сокращению накладных расходов. вызовов виртуальных функций. Грязный подход к этому состоял бы в том, чтобы иметь в каждом производном экземпляре идентификатор, представляющий его тип, который использовался бы для индексации таблицы переходов (массив указателей меток). Проблема в том, что:
- Стандартные таблицы переходов отсутствуют. C ++
- Требуется изменить все таблицы переходов при добавлении нового производного класса.
Я был бы благодарен, если бы кто-то придумал какой-то тип магии шаблонов (или макрос в качестве крайней меры), который позволил бы написать его так, чтобы он был более чистым, расширяемым и автоматизированным, например: