< быстрее <= в C++? Развенчиваем мифы с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: Миф, который живёт десятилетиями

Если вы когда-нибудь заглядывали на Stack Overflow в раздел C++, то наверняка видели вопрос: «Is < faster than <=?». Этот вопрос собрал тысячи просмотров и десятки страниц обсуждений. И неудивительно — тема оптимизации кода будоражит умы программистов всех уровней.

Логика кажется железобетонной: оператор < сравнивает два числа и возвращает результат, а <= делает то же самое, но дополнительно проверяет равенство. Значит, < должен работать быстрее, верно? Особенно это важно для циклов, где операция сравнения выполняется миллионы раз.

Сегодня мы раз и навсегда разберёмся с этим мифом. Вы узнаете:

  • Как на самом деле работают операторы на уровне процессора.
  • Почему компилятор — ваш лучший друг (или враг, если вы ему не доверяете).
  • Когда микрооптимизация имеет смысл, а когда — пустая трата времени.

Как работают операторы < и <= на уровне ассемблера?

Чтобы понять, есть ли разница, нужно заглянуть «под капот» — в то, во что компилятор превращает ваш C++ код. Рассмотрим две простые функции:

bool less_than(int a, int b) {
    return a < b;
}

bool less_or_equal(int a, int b) {
    return a <= b;
}

Скомпилируем их с помощью g++ (без оптимизаций, флаг -O0). Вот что мы увидим в ассемблерном листинге (x86-64):

less_than(int, int):
    cmp    edi, esi      ; сравнить a и b
    setl   al            ; установить al=1 если a < b (меньше)
    ret

less_or_equal(int, int):
    cmp    edi, esi      ; сравнить a и b
    setle  al            ; установить al=1 если a <= b (меньше или равно)
    ret

Видите? Единственная разница — инструкции setl (set if less) и setle (set if less or equal). Обе инструкции выполняются за один такт процессора. На современных CPU (Intel Core i7, AMD Ryzen) они имеют идентичную задержку — 1 цикл.

Вывод №1: На уровне машинных инструкций между < и <= нет разницы в производительности. Точка.

А что насчёт циклов? Тут-то разница должна быть!

Чаще всего спор разгорается вокруг циклов. Сравните два подхода:

// Вариант 1: с <
for (int i = 0; i < n; ++i) {
    // делаем что-то
}

// Вариант 2: с <=
for (int i = 0; i <= n - 1; ++i) {
    // делаем то же самое
}

Интуиция подсказывает: второй вариант делает лишнее вычисление n - 1. Но давайте посмотрим, что происходит после оптимизации компилятором.

Запустим бенчмарк с g++ -O2 для n = 1 000 000 000:

#include <iostream>
#include <chrono>

int main() {
    const long long n = 1'000'000'000;
    volatile long long sink = 0; // чтобы компилятор не выкинул цикл

    auto start = std::chrono::high_resolution_clock::now();
    
    for (long long i = 0; i < n; ++i) {
        sink += i;
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "Время (<): " 
              << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              << " ms\n";

    start = std::chrono::high_resolution_clock::now();
    
    for (long long i = 0; i <= n - 1; ++i) {
        sink += i;
    }
    
    end = std::chrono::high_resolution_clock::now();
    std::cout << "Время (<=): " 
              << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              << " ms\n";

    return 0;
}

Результаты на моей машине (i7-10750H, Linux, g++ 11.3):

  • <: 1823 ms
  • <=: 1821 ms

Разница — в пределах погрешности измерений. Практически идентично.

Почему компилятор делает их равными?

Современные компиляторы (g++, clang, MSVC) на уровнях оптимизации -O1, -O2, -O3 выполняют мощные преобразования циклов. Они:

  • Заменяют i <= n-1 на i < n (и наоборот), если это безопасно.
  • Используют индуктивные переменные — компилятор может заменить сравнение с переменной на сравнение с константой.
  • Применяют развёртку циклов, что делает единичные сравнения незначимыми.

Более того, многие процессоры имеют конвейерную архитектуру и предсказание ветвлений. Сравнение — настолько дешёвая операция, что её стоимость «растворяется» в общем потоке выполнения.

Вывод №2: Компилятор умнее нас. Доверяйте ему, если только вы не пишете код для очень специфичного железа (микроконтроллеры, старые архитектуры).

Когда разница всё-таки может проявиться?

Есть пара сценариев, где выбор оператора может повлиять на производительность, но они связаны не со скоростью самого сравнения.

1. Целочисленное переполнение

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

unsigned int n = 0;
for (unsigned int i = 0; i <= n - 1; ++i) {  // Внимание!
    // ...
}

Если n = 0, то n - 1 даёт максимальное значение unsigned int (4294967295). Цикл выполнится 4 миллиарда раз вместо нуля! Это катастрофа по времени, но проблема не в операторе, а в логической ошибке.

Использование < с i < n при n = 0 просто не запустит цикл. Поэтому безопаснее использовать < для счётчиков

Блоги

Рекомендаций по книгам