Introducción: Un mito que vive durante décadas
\n\nSi alguna vez has mirado en Stack Overflow la sección de C++, seguro que has visto la pregunta: «¿Es < más rápido que <=?». Esta pregunta ha acumulado miles de visitas y decenas de páginas de discusión. Y no es de extrañar: el tema de la optimización del código cautiva las mentes de programadores de todos los niveles.
\n\nLa lógica parece de hierro: el operador < compara dos números y devuelve el resultado, mientras que <= hace lo mismo, pero además verifica la igualdad. Por lo tanto, < debería ser más rápido, ¿verdad? Esto es especialmente importante en bucles, donde la operación de comparación se ejecuta millones de veces.
Hoy vamos a desentrañar este mito de una vez por todas. Aprenderás:
\n- \n
- Cómo funcionan realmente los operadores a nivel de procesador. \n
- Por qué el compilador es tu mejor amigo (o enemigo, si no confías en él). \n
- Cuándo la microoptimización tiene sentido y cuándo es una pérdida de tiempo. \n
¿Cómo funcionan los operadores < y <= a nivel de ensamblador?
\n\nPara entender si hay alguna diferencia, hay que mirar «bajo el capó»: en lo que el compilador convierte tu código C++. Consideremos dos funciones 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\nLas compilaremos con g++ (sin optimizaciones, bandera -O0). Esto es lo que vemos en el listado de ensamblador (x86-64):
less_than(int, int):\n cmp edi, esi ; comparar a y b\n setl al ; establecer al=1 si a < b (menor que)\n ret\n\nless_or_equal(int, int):\n cmp edi, esi ; comparar a y b\n setle al ; establecer al=1 si a <= b (menor o igual que)\n ret\n\n¿Ves? La única diferencia son las instrucciones setl (set if less) y setle (set if less or equal). Ambas instrucciones se ejecutan en un ciclo de reloj del procesador. En las CPU modernas (Intel Core i7, AMD Ryzen) tienen una latencia idéntica: 1 ciclo.
Conclusión n.º 1: A nivel de instrucciones de máquina, no hay diferencia de rendimiento entre < y <=. Punto.
¿Y qué pasa con los bucles? ¡Ahí debería haber diferencia!
\n\nLa mayoría de las veces, el debate se centra en los bucles. Compara estos dos enfoques:
\n\n// Opción 1: con <\nfor (int i = 0; i < n; ++i) {\n // hacer algo\n}\n\n// Opción 2: con <=\nfor (int i = 0; i <= n - 1; ++i) {\n // hacer lo mismo\n}\n\nLa intuición sugiere: la segunda opción hace un cálculo extra n - 1. Pero veamos qué sucede después de la optimización del compilador.
Ejecutemos un benchmark con 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 que el compilador no elimine el bucle\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 << "Tiempo (<): " \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 << "Tiempo (<=): " \n << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()\n << " ms\\n";\n\n return 0;\n}\n\nResultados en mi máquina (i7-107