Подтвердить что ты не робот

Эффект goto на оптимизацию компилятора С++

Каковы преимущества производительности или штрафы при использовании goto с помощью современного компилятора С++?

Я пишу генератор кода С++, и использование goto упростит запись. Никто не коснется результирующих файлов С++, поэтому не получайте от меня все "goto is bad" . В качестве преимущества они сохраняют использование временных переменных.

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

4b9b3361

Ответ 1

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

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

Другими словами, если (например) вы взяли код, который был написан с обычными for, while, switch и т.д., и преобразовали его, чтобы использовать goto в каждом случае, но сохраняли такая же структура, почти любой разумно современный компилятор, вероятно, создаст по существу идентичный код в любом случае. Если, однако, вы используете goto для создания беспорядка спагетти, как и многие из FORTRAN, которые мне приходилось смотреть несколько десятилетий назад, тогда компилятор, вероятно, не сможет многое с ним поделать.

Ответ 2

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

Почему вас это волнует? Ваша основная задача - получить генератор кода для создания правильного кода. Эффективность имеет гораздо меньшее значение, чем правильность. Ваш вопрос должен быть "Будет ли мое использование gotos сделать мой сгенерированный код более вероятным или менее вероятным?"

Посмотрите на код, сгенерированный lex/yacc или flex/bison. Этот код переполнен глотками. Для этого есть веская причина. lex и yacc реализуют конечные автоматы. Поскольку машина переходит в другое состояние при переходах состояний, goto, возможно, является наиболее естественным инструментом для таких переходов.

Существует простой способ устранить эти gotos во многих случаях, используя цикл while вокруг инструкции switch. Это структурированный код. Per Douglas Jones (Jones D. W., How (not) для кодирования конечного автомата, SIGPLAN Not. 23, 8 (Aug. 1988), 19-22.), Это худший способ кодирования FSM. Он утверждает, что схема на основе goto лучше.

Он также утверждает, что существует еще лучший подход, который преобразует ваш FSM в блок-схему управления с использованием методов теории графов. Это не всегда легко. Это проблема NP. Вот почему вы все еще видите множество FSM, в частности автогенерируемых FSM, реализованных либо как цикл вокруг коммутатора, либо с переходами состояний, реализованными через gotos.

Ответ 3

Как вы думаете, какие циклы представлены на уровне сборки?

Использование инструкций перехода к меткам...

Многие компиляторы действительно будут использовать скачки даже в своем промежуточном представлении:

int loop(int* i) {
  int result = 0;
  while(*i) {
    result += *i;
  }
  return result;
}

int jump(int* i) {
  int result = 0;
  while (true) {
    if (not *i) { goto end; }
    result += *i;
  }

end:
  return result;
}

Выход в LLVM:

define i32 @_Z4loopPi(i32* nocapture %i) nounwind uwtable readonly {
  %1 = load i32* %i, align 4, !tbaa !0
  %2 = icmp eq i32 %1, 0
  br i1 %2, label %3, label %.lr.ph..lr.ph.split_crit_edge

.lr.ph..lr.ph.split_crit_edge:                    ; preds = %.lr.ph..lr.ph.split_crit_edge, %0
  br label %.lr.ph..lr.ph.split_crit_edge

; <label>:3                                       ; preds = %0
  ret i32 0
}

define i32 @_Z4jumpPi(i32* nocapture %i) nounwind uwtable readonly {
  %1 = load i32* %i, align 4, !tbaa !0
  %2 = icmp eq i32 %1, 0
  br i1 %2, label %3, label %.lr.ph..lr.ph.split_crit_edge

.lr.ph..lr.ph.split_crit_edge:                    ; preds = %.lr.ph..lr.ph.split_crit_edge, %0
  br label %.lr.ph..lr.ph.split_crit_edge

; <label>:3                                       ; preds = %0
  ret i32 0
}

Где br - инструкция ветвления (условный переход).

В этой структуре выполняются все оптимизации. Итак, goto - это хлеб и масло оптимизаторов.

Ответ 4

Я полностью согласен с ответом Дэвида Хэммена, но у меня есть только один пункт, чтобы добавить.

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

Их не учат, что фактическое значение этого зависит от того, кем является пользователь.

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

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