Goto - вред или быстродействие?

Споры по поводу оправданности использования оператора goto в языках высокого уровня ведутся ещё с 1968 года, то есть больше 50 лет. Придти к окончательному выбору невозможно, поскольку как использование, так и полный отказ от оператора goto имеет свои достоинства и недостатки.

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

Аргументы против goto

Избыточность

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

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

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

Нечитабельный код

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

Документировать код с goto гораздо сложнее и к сожалению, далеко не все это делают.

Сложности при оптимизации

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

Аргументы за goto

Возможности

Есть ряд редких ситуаций, когда без goto трудно обойтись, например:

  1. Выход из вложенных циклов одновременно.
  2. Нелинейные циклы.
  3. Переход по dispatch-таблице без выделения дополнительной памяти (использования полиморфизма).
  4. Конечные автоматы.

К сожалению, многие программисты злоупотребляют возможностями, например, языка Си, где есть goto и используют этот оператор там, где он совершенно не нужен.

Выход из вложенных циклов

int i, j;
for (i = 0; i < N; i++) {
    for (j = 0; j < N; j++) {
        if (!arr[i][j]) {
            goto end; // break вышел бы только из текущего цикла
        }
    }
}
end:
// какие-то ещё действия

Стоит заметить, что в некоторых языках эта проблема решена.

Например, в php можно сделать следующим образом:

for ($i = 0; $i < $n; $i++) {
    for ($j = 0; $j < $n; $j++) {
    	if (!$arr[$i][$j]) {
            break 2;
        }
    }
}

Конечный автомат

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

state1: {
    switch (input) {
        case 1:
            goto state2;
        case 2:
            goto state3;
        default:
            goto unknown;
    }
}
state2: {
    switch (input) {
        case 0:
            goto state1;
        case 2:
            goto state3;
        default:
            goto unknown;
    }
}
state3: {
    switch (input) {
        case 0:
            goto state1;
        case 1:
            goto state2;
        default:
            goto unknown;
    }
}
unknown: {
    // ...
}

Обратите внимание на оформление кода. Каждой метке соответствует свой блок. Благодаря этому код легко читается и мы понимаем, внутри обработчика какого состояния мы находимся.

Динамический dispatch

GCC компилятор поддерживает создание массивов с метками и переход по динамически выбранной метке.

Пример:

void* marks[3] = {
    &&mark_0,
    &&mark_1,
    &&mark_2
};

int instruction;

for (;;) {
    instruction = /*...*/;
    
    goto *marks[instruction];

    mark_0:
    // ...
    mark_1:
    // ...
    mark_2:
    // ...
}

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

Нелинейные циклы

Рассмотрим следующий граф выполнения кода:

В данном случае каждое ребро графа иллюстрирует какой-то кусок кода, а вершина - решение.

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

В данном случае это нелинейный цикл с одним кольцом, но их могло быть больше.

Такой цикл невозможно запрограммировать без использования goto и переменных состояния.

Итог

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

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

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

  1. Не нужно перемешивать метки и операторы goto, не делая отступы и не выделяя свой блок на ключевые метки.

  2. Переходить желательно только вперёд (за исключением конечного автомата).

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


Copyright © 2019 Александр Майоров. Все права защищены.