Avatar ·

Операция сравнения < быстрее, чем <=? Влияет ли выбор оператора на производительность в C++?

Всем привет! Я начинающий C++ разработчик, и меня мучает один вопрос уже несколько дней. Я пишу циклы с условиями, и часто задумываюсь: а есть ли разница в производительности между `i < n` и `i <= n`? Например, на ассемблерном уровне эти операции могут выполняться по-разному. Я слышал, что на старых процессорах сравнение на "меньше" могло быть быстрее, чем "меньше или равно", потому что второе требует дополнительной проверки равенства. Но сейчас, в 2024 году, это всё ещё актуально? Вот простой пример кода, который меня волнует: ```cpp int sum = 0; for (int i = 0; i < 1000000; ++i) { sum += i; } ``` vs ```cpp int sum = 0; for (int i = 0; i <= 999999; ++i) { sum += i; } ``` Оба цикла делают одно и то же, но первый использует `<`, а второй `<=`. Компилятор оптимизирует это? Или мне нужно всегда предпочитать один оператор другому? Особенно интересует, как это работает на современных процессорах x86-64 и ARM. Может быть, есть случаи, когда разница действительно заметна? Заранее спасибо за разъяснение! <<>>

Краткий ответ: нет, разницы в производительности нет

На современных компиляторах и процессорах выбор между < и <= не влияет на скорость выполнения. Компилятор оптимизирует оба варианта до одинакового машинного кода. Если когда-то (в эпоху 8-битных процессоров) и была разница, то сейчас это миф из прошлого.

Почему так происходит?

Процессоры x86-64 и ARM имеют инструкции для всех типов сравнений: JL (jump if less), JLE (jump if less or equal), JGE и т.д. Все они выполняются за одинаковое количество тактов (обычно 1 такт). Более того, компиляторы часто преобразуют i <= n в i < n+1 или наоборот, чтобы получить более эффективный код.

Проверим на практике

Рассмотрим два цикла:

// Вариант 1: i < n
int sum1 = 0;
for (int i = 0; i < 1000000; ++i) {
    sum1 += i;
}

// Вариант 2: i <= n
int sum2 = 0;
for (int i = 0; i <= 999999; ++i) {
    sum2 += i;
}

Скомпилируем с -O2 (g++ или clang++) и посмотрим на ассемблер. Оба варианта дадут практически идентичный код:

; Примерный ассемблер для x86-64
xor eax, eax          ; sum = 0
xor ecx, ecx          ; i = 0
.L2:
add eax, ecx          ; sum += i
add ecx, 1            ; ++i
cmp ecx, 1000000      ; сравнение с границей
jl .L2                ; jump if less (или jle для второго варианта)

Единственное отличие — инструкция jl vs jle. Но обе выполняются за 1 такт на современных процессорах (Intel Core, AMD Ryzen).

Что насчёт вложенных циклов и сложных условий?

Если условие сложнее (например, if (a < b && c <= d)), компилятор может переставить сравнения или объединить их. Но опять же, выбор оператора не даст преимущества.

Когда разница может быть заметна?

  • На очень старых процессорах (Intel 8086, Z80) — там сравнение на равенство могло быть дороже. Но такие CPU уже не используются в современном софте.
  • В циклах с инкрементом на 1 — компилятор может заменить i <= n на i < n+1, если n+1 не вызывает переполнения. Но это не делает код быстрее или медленнее.
  • Если граница цикла — отрицательное число — тут может возникнуть проблема со знаковым переполнением, но это уже вопрос корректности, а не производительности.

Вывод

Не заморачивайтесь на выбор между < и <=. Пишите код так, чтобы он был читаемым и логичным. Если вам нужно выполнить итерации от 0 до 99, используйте i < 100. Если от 0 до 100 включительно — i <= 100. Разницы в скорости нет.

Единственное, что действительно влияет на производительность циклов — это:

  • Минимизация вызовов функций в условии (например, for (int i = 0; i < vec.size(); ++i) лучше заменить на for (size_t i = 0, end = vec.size(); i < end; ++i)).
  • Использование префиксного инкремента ++i вместо постфиксного i++ (для итераторов).
  • Разворачивание циклов (loop unrolling) — но это компилятор делает сам.
<<>>

Дополнение: когда выбор оператора всё же может иметь значение

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

Рассмотрим пример:

unsigned int n = 0;
for (unsigned int i = 0; i <= n; ++i) {
    // что-то делаем
}

Этот цикл выполнится один раз (при i=0). А если бы мы написали i < n — ни разу. В случае с unsigned int и границей n = 0 разница есть, но она семантическая, а не производительностная.

Другой пример — работа с итераторами:

std::vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it <= v.end(); ++it) {  // Ошибка! 
    // ...
}

Для итераторов operator<= может быть определён, а может и нет. Для обычных указателей он работает, но для итераторов из std::list или std::map — нет. Поэтому лучше использовать it != v.end() или it < v.end() (только для RandomAccess итераторов).

Резюме

Разницы в скорости между < и <= нет. Выбирайте оператор исходя из логики алгоритма, а не из соображений производительности. Компилятор сделает всё оптимально.