benchmark-count-one-billion

¿Cuanto tiempo cuesta contar a un billón en distintos lenguajes?

Recientemente se ha hecho viral en Internet un meme con una captura de una tabla comparando velocidades de distintos lenguajes, sumando todos los números desde uno hasta un billón (inglés, 1.000 millones) en un bucle simple.

Por supuesto, el hilo se ha llenado de gente dando su opinión, haciendo sus conjeturas… aunque muchas no tengan ni pies ni cabeza. Y, como suele pasar, algunos comentarios son … digamos “apasionados”. Vamos, que había más hate, que cabeza.

Antes de nada, que quede claro que mi opinión es que este tipo de discusiones de “mi lenguaje es mejor que el tuyo” son bastante infantiles. Hay aspectos mucho más importantes a la hora de elegir un lenguaje, y centrarse en algo como “cuenta hasta 1 billón más rápido” es muy muy muy limitado.

Pero bueno a la mayoría de la gente le encanta hablar del asunto y comparar lenguajes, y hacerlos competir entre ellos. Es más, confesemos que todos lo hacemos de vez en cuando.

Así que vamos a hacer una entradita al respecto, con un mínimo de rigor (al rigor que nos deje que simplemente vamos a contar un billón).

Benchmark entre lenguajes

En primer lugar, que quede claro que hacer un benchmark preciso y representativo es bastante complicado. Depende de muchos factores, como el hardware utilizado, la implementación específica de cada lenguaje, las optimizaciones aplicadas, hasta del momento en que pillas al procesador. Es todo un mundo hacer benchmark bien hechos.

Pero nada, eso no interesa ¡lo que queremos es contar hasta un billon! Pues venga, vamos a contar hasta un billón en distintos lenguajes. Que como benchmark no deja de ser una mierda como una casa, pero… por otro lado no deja de ser un test como otro cualquiera.

Así que vamos a comparar,

  • C++, compilado con MVSC, en Debug / Release
  • C++, compilado con G++, con y sin optimizaciones -O3
  • Go
  • Rust, compilado en modo Debug / Release
  • JavaScript ejecutado sobre NodeJS
  • JavaScript ejecutado sobre Chrome
  • Python

Y los resultados los tenéis aquí, con el tiempo en segundos (menos es mejor),

LanguageTime (s)
C++ (G++ -O3)0
Rust (Release)0
C++ (MVSC Release)0,1
Go0,25
C# (Release)0,32
C++ (MVSC Debug)0,5
JS (NodeJS)0,45
C# (Debug)0,65
C++ (G++)1,3
JS (Chrome)4,35
Rust (Debug)6,46
Python40,46

Evidentemente, estos tiempos sólo sirven para mi ordenador i5 Gen 12, Win10, a las 2 de la noche, mientras escucho música en Spotify, y solo son válidos para comparar entre ellos.

Si lo ejecutas en tu ordenador, tendrás otros resultados. Que estoy viendo gente compartir tiempos entre ellos como si todos tuvieran el mismo ordenador, diciendo pues a mi “tal test” me da “tanto”. ¡Y yo que sé que ordenador tienes! Estás comparando manzanas con peras. Tendrás que ejecutar todos los test y comparar entre ellos.

En cualquier caso, vamos a ponerlo en una grafiquita bonita, y queda,

lenguaje-comparativa-chart

Aquí con ampliación a la parte baja de la tabla (porque Python es tan cenutrio que me jode toda la gráfica, y no deja ver nada)

lenguaje-comparativa-chart-2

Cosas que veremos. Python es el más lento, por amplia diferencia. ¡Oh, que sorpresa! Todos sabíamos que Python era lento, y que su propósito no es ser rápido.

En cuanto a los más rápidos, C++ y Rust. Lógico, porque son para eso. Es más, en modo optimizado (Release o -O3) el tiempo es CERO. No “más o menos pequeño” … es CERO. Eso es porque el compilador es lo suficiente inteligente para eliminar por completo el bucle, porque sabe que su valor es N*(N+1)/2.

¿Te sorprende que el compilador sea capaz de eliminar todo el bucle? Mira esta entrada Ponemos a prueba como de listo es el compilador de Arduino

Cosa que solo es posible porque este benchmark es excesivamente sencillo, y el compilador puede eliminar el bucle. Si el bucle no fuera tan sencillo no podría eliminarlo. Pero bueno, así ilustramos (¡otra vez!) lo importante que es el papel del compilador y sus optimizaciones.

También atentos a la diferencia entre compilar con Release, o Debug (optimizado - no optimizado). Rust sin optimizar se va a 6 segundos, y C++ en G++ hasta 1.3 segundos. Ósea eso de “mi lenguaje es muy rápido”. Buenos sí… si lo compilas bien.

Muchas veces ves a gente diciendo que C++ es muy rápido y “metiéndose” con JavaScript (por ejemplo)… pero luego se dejan el flag -O3. Pues entonces tu código de C++ ¡es el triple de lento que el de JavaScript!

Continuamos para bingo con C# el nene bonito de esta santa casa (mi web), que saca 0.66 en Debug y 0.32 en Release. Nuevamente, importante la diferencia entre Debug y Release, y en general muy buen resultado.

Eso sí, me decepciona un poquito que el compilador de .NET no haya sido capaz de detectar que el bucle ese no sirve de nada, y no lo haya eliminado como Rust y C++. Pues es lo que hay ¿veis? Así se acepta un resultado, incluso cuando no te gusta especialmente.

Por otro lado, veo a mucha gente en los comentarios diciendo “es imposible que C# gane a C++“. Eh, sí, sí es posible. Yo mismo lo he visto. A veces (no con frecuencia, pero a veces) el código de C# gana en velocidad al de C++. ¿No te lo crees? Busca compilador JIT en internet, y ya tienes lectura y entretenimiento para un rato.

JavaScript, en navegador es obviamente un desastre. Pero en NodeJS saca 0.45s, muy buen resultado (punto para Node). Un buen ejemplo de que el lenguaje no es lo único que importa. De hecho, el lenguaje es lo de menos. Es el entorno / compilador / interprete el que tiene mayor influencia en la velocidad.

Casi siempre el lenguaje está unido a un entorno de ejecución y/o framework. Pero conceptualmente, son independientes, el lenguaje es un concepto abstracto. Por eso todos los lenguajes sacan distintos tiempos, en función de cómo los ejecutes. Este es uno de los motivos por los que comparar “lenguajes” no tiene mucho sentido.

Por último, y ya termino la chapa, Go saca un muy honroso 0.25s. Colocándose en algo más rápido que C#, y algo más lento que C++ y Rust. Pues ahí está, si te gusta Go, no ha salido nada mal en la comparativa.

Conclusión

Conclusiiiioooooon. Pues eso, los resultados son los que son. Algunos dirán “¡Es que mi lenguaje se puede optimizar haciendo…tal y cual cosa! y ñiñiñiñiñi” ¡Sí hombre, claro! Todos los lenguajes se pueden optimizar en esta vida. Pero lo que ha salido, es lo que sale, en el “flujo de ejecución normal” del lenguaje.

¿Eres fanático de X lenguaje y no te gusta que ha salido más lento que C++ / Rust ? Pues, amigo mío… a aguantarse 🤷. No seas fanático de un lenguaje, que no tiene sentido. Y los datos son los que son. ¿No te gusta los resultados del benchmark? Perfecto, haz el tuyo propio, lo compartes con la comunidad, y así todos podemos aprender algo 😙.

Pero sobre todo, de verdad, no os toméis estas cosas tan, tan en serio. Son solo comparaciones entre lenguajes. No es un miembro de tu familia. No es alguien que ha atropellado a tu gato 🐈. No eres un hincha de un partido de fútbol.

Cada lenguaje tiene sus características. Defender uno u otro, o meterse con uno o con otro, no tiene demasiado sentido. Además, en el mundo real las cosas son mucho más complicadas (y por otro lado divertidas) que “cuenta hasta un billón”.

Así que ser buenos, aprended todos los lenguajes que podáis, y no vayas discutiendo con la gente. ¡A cuidarse y portarse bien 😘!

Código empleado

Aquí os dejo el código que he empleado, para que puedas probar en tu ordenador, sin trampa ni cartón.

C#

using System.Diagnostics;

Stopwatch stopwatch = Stopwatch.StartNew();

long suma = 0;
for (int i = 0; i <= 1000000000; i++)
{
    suma += i;
}

stopwatch.Stop();
TimeSpan duration = stopwatch.Elapsed;

Console.WriteLine(suma);
Console.WriteLine("Tiempo transcurrido: " + duration.TotalSeconds + " segundos");

C++

#include <iostream>
#include <chrono>

int main() {
    auto start = std::chrono::high_resolution_clock::now();
    
    long long suma = 0;
    for (int i = 0; i <= 1000000000; i++) {
        suma += i;
    }
    
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> duration = end - start;
    
    std::cout << suma;
    std::cout << "Tiempo transcurrido: " << duration.count() << " segundos" << std::endl;
    
    return 0;
}

JavaScript

console.time("Tiempo transcurrido");

let suma = 0;
for (let i = 0; i <= 1000000000; i++) {
	suma += i;
}

console.timeEnd("Tiempo transcurrido");

Python

import time

start_time = time.time()

suma = 0;
for i in range(1000000000):
    suma += i

end_time = time.time()
duration = end_time - start_time

print(suma);
print("Tiempo transcurrido: ", duration, "segundos")

Rust

use std::time::Instant;

fn main() {
    let start_time = Instant::now();
    let mut sum: i64 = 0;

    for i in 1..=1_000_000_000 {
        sum += i;
    }

    let elapsed_time = start_time.elapsed();

    println!("La suma total es: {}", sum);
    println!("Tiempo transcurrido: {:?}", elapsed_time);
}
  • Debug con cargo run
  • Build con cargo run —release

Go

package main

import (
	"fmt"
	"time"
)

func main() {
	startTime := time.Now()
	sum := 0

	for i := 1; i <= 1000000000; i++ {
		sum += i
	}

	elapsedTime := time.Since(startTime)

	fmt.Println("La suma total es:", sum)
	fmt.Println("Tiempo transcurrido:", elapsedTime)
}