Introdução: O mito que vive há décadas
\n\nSe você já deu uma olhada no Stack Overflow na seção de C++, provavelmente já viu a pergunta: «Is < faster than <=?». Esta pergunta acumulou milhares de visualizações e dezenas de páginas de discussões. E não é surpresa — o tópico de otimização de código agita as mentes de programadores de todos os níveis.
\n\nA lógica parece de concreto armado: o operador < compara dois números e retorna o resultado, enquanto <= faz o mesmo, mas verifica adicionalmente a igualdade. Então, < deve ser mais rápido, certo? Isso é especialmente importante para loops, onde a operação de comparação é executada milhões de vezes.
Hoje vamos desvendar esse mito de uma vez por todas. Você descobrirá:
\n- \n
- Como os operadores realmente funcionam no nível do processador. \n
- Por que o compilador é seu melhor amigo (ou inimigo, se você não confiar nele). \n
- Quando a micro-otimização faz sentido e quando é perda de tempo. \n
Como os operadores < e <= funcionam no nível do assembly?
\n\nPara entender se há diferença, precisamos olhar "sob o capô" — no que o compilador transforma seu código C++. Vamos considerar duas funções simples:
\n\nbool less_than(int a, int b) {\n return a < b;\n}\n\nbool less_or_equal(int a, int b) {\n return a <= b;\n}\n\nVamos compilá-las com g++ (sem otimizações, flag -O0). Aqui está o que vemos na listagem assembly (x86-64):
less_than(int, int):\n cmp edi, esi ; comparar a e b\n setl al ; definir al=1 se a < b (menor que)\n ret\n\nless_or_equal(int, int):\n cmp edi, esi ; comparar a e b\n setle al ; definir al=1 se a <= b (menor ou igual)\n ret\n\nViu? A única diferença são as instruções setl (set if less) e setle (set if less or equal). Ambas as instruções são executadas em um ciclo do processador. Em CPUs modernas (Intel Core i7, AMD Ryzen), elas têm latência idêntica — 1 ciclo.
Conclusão nº 1: No nível das instruções de máquina, não há diferença de desempenho entre < e <=. Ponto final.
E quanto aos loops? Deve haver diferença aqui!
\n\nNa maioria das vezes, a discussão gira em torno de loops. Compare duas abordagens:
\n\n// Opção 1: com <\nfor (int i = 0; i < n; ++i) {\n // faz algo\n}\n\n// Opção 2: com <=\nfor (int i = 0; i <= n - 1; ++i) {\n // faz a mesma coisa\n}\n\nA intuição sugere: a segunda opção faz um cálculo extra n - 1. Mas vamos ver o que acontece após a otimização do compilador.
Vamos executar um benchmark com g++ -O2 para n = 1 000 000 000:
\n\n#include <iostream>\n#include <chrono>\n\nint main() {\n const long long n = 1'000'000'000;\n volatile long long sink = 0; // para o compilador não eliminar o loop\n\n auto start = std::chrono::high_resolution_clock::now();\n \n for (long long i = 0; i < n; ++i) {\n sink += i;\n }\n \n auto end = std::chrono::high_resolution_clock::now();\n std::cout << "Tempo (<): " \n << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()\n << " ms\\n";\n\n start = std::chrono::high_resolution_clock::now();\n \n for (long long i = 0; i <= n - 1; ++i) {\n sink += i;\n }\n \n end = std::chrono::high_resolution_clock::now();\n std::cout << "Tempo (<=): " \n << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()\n << " ms\\n";\n\n return 0;\n}\n\nResultados na minha máquina (i7-107