Почему переключатель не оптимизирован так же, как цепочка, если еще в c / c ++?

39

Следующая реализация square производит серию операторов cmp / je, как я и ожидал от цепочки if:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

И следующее производит таблицу данных для возврата:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

Почему gcc не может оптимизировать верхний в нижний?

Разборка для справки: https://godbolt.org/z/UP_igi

РЕДАКТИРОВАТЬ: интересно, MSVC генерирует таблицу переходов вместо таблицы данных для случая коммутатора. И что удивительно, clang оптимизирует их до того же результата.

chacham15
источник
3
Что вы имеете в виду "неопределенное поведение"? Пока наблюдаемое поведение одинаково, компилятор может генерировать любую сборку / машинный код, который он хочет
bolov
2
@ user207421 игнорируя returns; случаи не имеют breaks, таким образом, у коммутатора также есть определенный порядок выполнения. Цепочка if / else имеет возвраты в каждой ветви, семантика в этом случае эквивалентна. Оптимизация не невозможна . Как контрпример, ICC не оптимизирует ни одну из функций.
user1810087
9
Возможно, самый простой ответ ... gcc просто не может увидеть эту структуру и оптимизировать ее (пока).
user1810087
3
Я согласен с @ user1810087. Вы просто нашли текущую границу процесса уточнения компилятора. Под-кейс, который в настоящее время не распознается как оптимизируемый (некоторыми компиляторами). Фактически, не каждая цепочка else-if может быть оптимизирована таким образом, а только подмножество, в котором переменная SAME проверяется на постоянные значения.
Роберто Кабони
1
У if-else другой порядок выполнения, сверху вниз. Тем не менее, замена кода просто, если операторы не улучшают машинный код. Переключатель, с другой стороны, не имеет заранее определенного порядка выполнения и, по сути, является просто прославленной таблицей переходов goto. При этом компилятору разрешается рассуждать о наблюдаемом поведении, поэтому плохая оптимизация версии if-else весьма разочаровывает.
Лундин

Ответы:

29

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

Теперь, чтобы прийти if-else, это полная противоположность. Хотя switch-caseвыполняется в постоянное время, независимо от количества ветвей, if-elseоптимизируется для меньшего количества веток. Здесь вы можете ожидать, что компилятор генерирует серию сравнений в том порядке, в котором вы их написали.

Так что, если бы я использовал, if-elseпотому что ожидал, что большинство вызовов square()будут для 0или 1редко для других значений, то «оптимизация» этого для поиска в таблице может фактически привести к тому, что мой код будет выполняться медленнее, чем я ожидал, что противоречит моей цели использования ifвместо о switch. Таким образом , хотя это спорно, я чувствую НКА делает правильную вещь и лязг является чрезмерно агрессивным в его оптимизации.

Кто-то в комментариях поделился ссылкой, где clang выполняет эту оптимизацию и также генерирует код на основе таблицы поиска if-else. Что-то примечательное происходит, когда мы уменьшаем количество дел до двух (и по умолчанию) с помощью clang. Он снова генерирует идентичный код для if и switch, но на этот раз переключается на сравнение и перемещается вместо метода таблицы поиска, для обоих. Это означает, что даже одобряющий переключение кланг знает, что шаблон «если» более оптимален, когда число случаев невелико!

Таким образом, последовательность сравнений if-elseи таблица переходов для switch-case- это стандартный шаблон, которому склонны следовать компиляторы, и разработчики ожидают, когда они пишут код. Однако для определенных особых случаев некоторые компиляторы могут отказаться от этого шаблона, если они считают, что он обеспечивает лучшую оптимизацию. Другие компиляторы могут в любом случае просто придерживаться шаблона, даже если он явно не оптимален, доверяя разработчику знать, чего он хочет. Оба являются подходящими подходами со своими преимуществами и недостатками.

th33lf
источник
2
Да, оптимизация - это обоюдоострый меч: что они пишут, что они хотят, что они получают, и кого мы за это ругаем.
дедупликатор
1
«... тогда« оптимизация »этого для поиска в таблице фактически приведет к тому, что мой код будет работать медленнее, чем я ожидаю…» Можете ли вы дать обоснование этому? Почему таблица переходов будет медленнее, чем две возможные условные ветви (для проверки входных данных 0и 1)?
Коди Грей
@CodyGray Я должен признаться, что я не дошел до уровня подсчета циклов - я просто испытывал чувство, что загрузка из памяти через указатель может занять больше циклов, чем сравнение и переход, но я могу ошибаться. Тем не менее, я надеюсь, что вы согласны со мной, что даже в этом случае, по крайней мере, для «0», ifочевидно, быстрее? Теперь вот пример платформы, где и 0, и 1 будут быстрее при использовании, ifчем при использовании switch: godbolt.org/z/wcJhvS (обратите внимание, что здесь также есть несколько других оптимизаций)
th33lf
1
Ну, в любом случае подсчет циклов не работает на современных суперскалярных архитектурах ООО. :-) Загрузка из памяти не будет медленнее, чем непредсказуемые ветви, поэтому вопрос заключается в том, насколько вероятна предсказание ветви? Этот вопрос относится ко всем типам условных переходов, будь то сгенерированные явными ifоператорами или автоматически компилятором. Я не эксперт по ARM, поэтому я не совсем уверен, что ваше заявление о том, что вы switchбыстрее, чем ifверно. Это будет зависеть от штрафа за неправильно предсказанные ветви, и это будет зависеть от того, какой ARM.
Коди Грей
0

Одним из возможных объяснений является то, что если более низкие значения numболее вероятны, например всегда 0, сгенерированный код для первого может быть быстрее. Сгенерированный код для переключения занимает одинаковое время для всех значений.

Сравнивая лучшие случаи, согласно этой таблице . Смотрите этот ответ для объяснения таблицы.

Если num == 0для «если» у вас есть xor, test, je (с прыжком), ret. Задержка: 1 + 1 + прыжок. Однако xor и test независимы, поэтому фактическая скорость выполнения будет выше, чем 1 + 1 циклов.

Если num < 7для «switch» у вас есть mov, cmp, ja (без прыжка), mov, ret. Задержка: 2 + 1 + без прыжка + 2.

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

ил
источник
1
Хм, интересная теория, но для ifs vs switch у вас есть: xor, test, jmp vs mov, cmp jmp. Три инструкции каждая с последним прыжком. Кажется равным в лучшем случае, нет?
chacham15
3
Msgstr "Инструкция перехода, которая не приводит к прыжку, быстрее, чем инструкция, которая приводит к прыжку." Это предсказание отрасли имеет значение.
Геза