Cómo comparar de forma justa la velocidad de ejecución de C#, C++, Java y Go
· Actualizado el: · Go Komura · Benchmark, Rendimiento, C#, C++, Java, Go
“Dicen que C++ es rápido.” “Go es ligero en producción.” “Java, si lo dejas correr mucho tiempo, va bastante rápido.” “C# también es sorprendentemente fuerte gracias al JIT de .NET.”
Este tipo de comentarios se escuchan a menudo. Sin embargo, lo peor que se puede hacer aquí es tomar cifras medidas por personas distintas, en entornos distintos, ponerlas una junto a otra y decidir así, sin más, qué lenguaje es superior.
C# y Java se ven fácilmente afectados por el JIT y el warm-up, mientras que C++ y Go suelen estar precompilados de antemano. También difiere la existencia y las características del GC. Las diferencias de implementación de la biblioteca estándar y de las bibliotecas relacionadas también pesan bastante. Y además, incluso en la misma máquina, la configuración de energía, el calor, los procesos en segundo plano o el sesgo de los datos de entrada hacen que los resultados fluctúen con facilidad. Es un terreno bastante ingrato.
En este artículo ordenamos la forma de medir para comparar C#, C++, Java y Go de la manera más justa posible. Si hay que adelantar la conclusión: lo más importante es no intentar decidir “qué lenguaje es el más rápido” con una sola cifra.
Este artículo se centra, ante todo, en ordenar el método de comparación. Poner en fila cifras que dependen del entorno no sirve de mucho, porque basta con que cambien las condiciones para que el orden se invierta con facilidad. Por eso aquí no se incluye un ranking con mediciones reales. En su lugar, nos concentramos en cómo diseñar la comparación para que tenga valor.
Público objetivo
Este artículo está escrito para desarrolladores y líderes técnicos que tienen varios lenguajes candidatos y quieren decidir con cuál implementar basándose en el rendimiento, y para quienes quieren montar una medición que se pueda presentar internamente o en un informe diciendo “hemos comparado la velocidad”. El tema principal no es profundizar en un lenguaje en concreto, sino el diseño de una comparación que cruce los 4 lenguajes, así que está pensado para poder leerse incluso si solo se trabaja con uno de ellos.
Los ejemplos de código usan PowerShell como runner común, y BenchmarkDotNet de C# y JMH de Java como harness dentro de cada lenguaje. Para C++ y Go solo se indica cómo igualar las condiciones.
Términos que conviene tener claros de antemano
En el texto aparecen varios términos que se dejan en inglés. Para no tropezar con ellos la primera vez que salen, los reunimos aquí de antemano.
| Término | Significado |
|---|---|
| p95 / p99 | Percentil. Si se ordenan todas las ejecuciones de más rápida a más lenta, es el valor que queda en la posición del 95 % / 99 % desde abajo. “5 de cada 100 ejecuciones son más lentas que esto” corresponde al p95 |
| RSS (Resident Set Size) | El tamaño que el proceso tiene realmente cargado en memoria física. No es la cantidad de memoria virtual reservada, sino “cuánta memoria real está ocupando ahora mismo” |
| LTO (Link Time Optimization) | Mecanismo que optimiza a través de las unidades de traducción en el momento del enlazado. Corresponde a -flto de GCC/Clang y a /GL y /LTCG de MSVC |
| PGO (Profile-Guided Optimization) | Mecanismo que usa el perfil de ramas y llamadas recogido en una ejecución previa para las decisiones de optimización de la siguiente compilación |
| Tiered Compilation | Mecanismo del JIT de .NET por el cual primero se ejecuta con código que se puede compilar rápido, y solo los métodos que se llaman con frecuencia se vuelven a optimizar más adelante. Es una de las causas principales de la diferencia entre cold y warm |
| Server GC / Workstation GC | Modos de funcionamiento del GC de .NET. Server GC tiene un heap y hilos de GC por cada procesador lógico y se orienta al throughput; Workstation GC se orienta más a la capacidad de respuesta |
| GOMAXPROCS | El límite del número de hilos del sistema operativo con los que el runtime de Go ejecuta código Go de forma simultánea. En benchmarks paralelos, si no se fija este valor, los resultados no se pueden comparar |
| cgo | Mecanismo que permite llamar código C desde Go. Al activarlo cambia el coste de la llamada, las condiciones de compilación y la posibilidad de enlazado estático |
Antes que nada, la conclusión
En la comparación de velocidad entre C#, C++, Java y Go, lo que realmente importa se reduce a estos 7 puntos.
-
Decidir primero qué velocidad se quiere comparar Si es el tiempo de arranque, el throughput en estado estable, la latencia p95 o la eficiencia de memoria, la forma de medir cambia.
-
No sacar una conclusión con un solo benchmark Según se trate de cálculo de CPU, asignación de memoria, procesamiento paralelo o tiempo de arranque, cambia qué lenguaje o runtime parece más fuerte.
-
Separar cold y warm en C# y Java Si se mezcla la comparación que incluye la primera ejecución con la comparación en estado estable tras el warm-up, el razonamiento se retuerce.
-
Medir con el mismo algoritmo, la misma entrada y la misma verificación de corrección Un clásico de los benchmarks es que, en lugar de tener una implementación más rápida, simplemente se estaba resolviendo un problema distinto.
-
Separar el microbenchmark dentro de cada lenguaje del benchmark end-to-end que cruza lenguajes El harness específico de cada lenguaje es cómodo, pero para la comparación entre lenguajes es mejor ejecutar con un runner común externo.
-
Mirar no solo la media, sino también la mediana y la distribución Basta con que el GC o un proceso en segundo plano se cuele una sola vez para que la media quede rota.
-
Dejar registradas no solo las cifras, sino también las condiciones Un resultado de benchmark es a la vez un registro de velocidad y un registro de las condiciones del experimento. Un resultado sin las condiciones anotadas resulta bastante penoso más adelante.
Lo primero que hay que decidir
Si “rápido” se despacha con una sola palabra, casi siempre acaba en accidente. Lo primero es decidir a qué se llama “rápido”.
Por ejemplo, incluso con el mismo programa, lo que se quiere observar puede ser bastante distinto.
1. ¿Se quiere ver el tiempo de arranque?
En una herramienta CLI, un batch de vida corta o una herramienta auxiliar que arranca una vez y termina enseguida, lo que importa es el cold start o el process startup. En este eje, el resultado cambia mucho según se incluya o no el coste de inicialización del JIT y de la carga de clases.
2. ¿Se quiere ver el throughput en funcionamiento prolongado?
En un servidor, un proceso residente, un worker o un procesamiento de conversión que se ejecuta durante mucho tiempo, lo importante es el throughput en steady-state. En este caso, que la primera ejecución sea lenta no es lo esencial; lo esencial es hasta dónde se estabiliza y crece después del warm-up.
3. ¿Se quiere ver la tail latency?
En APIs, UI o procesamiento cercano a tiempo real, a veces importa más el p95 / p99 que la media. Aunque la media sea rápida, si de vez en cuando se detiene mucho tiempo, la experiencia de usuario o el SLA lo sufren.
4. ¿Se quiere incluir también la eficiencia de memoria?
No basta con mirar el tiempo de CPU: hay que ver también el RSS máximo, la cantidad asignada, el número de GC y las pausas de GC, o se malinterpreta el peso real en producción. “Rápido pero come bastante memoria” y “algo más lento pero estable y ligero” son evaluaciones que se invierten según el uso.
En resumen, la pregunta que hay que decidir al principio es
Lo que se quiere saber con esta comparación no es qué lenguaje es rápido, sino qué workload, en qué condiciones y con qué indicador se puede procesar más rápido
Si se recogen cifras dejando esto ambiguo, al final no hay forma de darles sentido.
Por qué comparar lenguajes es difícil
Mezclar JIT y AOT es hacer otro experimento
C# y Java suelen recibir la influencia del JIT. C++ y Go, en cambio, suelen estar precompilados de antemano.
Es decir, si se mide la primera ejecución, no se mide solo la velocidad del propio programa, sino también el arranque del runtime, la carga de clases y la preparación del JIT. Al contrario, si solo se mira después de un warm-up suficiente, entonces la comparación pasa a ser sobre hasta dónde llega la optimización en estado estable.
Ambas cosas tienen sentido. Pero no es el mismo sentido.
Es habitual que la diferencia de implementación pese más que la de lenguaje
Aunque se trate del mismo “ordenar”,
- una parte usa la biblioteca estándar
- otra parte tiene una implementación propia
- otra parte hace copias de más
- otra parte regenera la entrada cada vez
Solo con esto, el resultado ya cambia bastante.
Además, en procesamientos como JSON, compresión, criptografía o expresiones regulares, la diferencia de implementación de biblioteca pesa bastante más que la del propio lenguaje. Por eso, si no se deja claro qué se está midiendo, lo que se pretendía que fuera una “comparación de lenguajes” acaba siendo una “comparación de bibliotecas”.
En C++ existe la trampa de que la optimización hace desaparecer el procesamiento
Especialmente en microbenchmarks, si el compilador decide que “a este resultado de cálculo no lo usa nadie”, puede eliminar el propio procesamiento. Entonces, el resultado no es que sea rápido, sino que en realidad no se está haciendo nada. El caso típico es que todo el bucle desaparece y el tiempo de ejecución queda prácticamente en cero.
En C++ este problema tiende a manifestarse de forma especialmente descarada, así que es bastante importante usar el resultado, emitir un checksum, o apoyarse en la función de los frameworks de benchmark que impide esta optimización.
La existencia del GC no es “desventaja” ni “ventaja”, sino una característica
C#, Java y Go tienen GC. Reducirlo sin más a “es lento porque tiene GC” es demasiado simplista.
En realidad, lo que más pesa es
- cómo se gestiona la gran cantidad de objetos de vida corta
- la configuración del tamaño del heap
- la frecuencia de GC y sus pausas
- el layout de los objetos
- los hábitos de asignación de las bibliotecas
En cambio, C++ permite un control fino con gestión manual o RAII, pero por eso mismo las diferencias de diseño e implementación se notan más. Es decir, que la forma de gestión sea distinta no es, tal cual, ni bueno ni malo, ni superioridad ni inferioridad.
Lo que no se debe hacer al comparar
1. Mezclar Debug y Release
Esto está totalmente fuera de lugar. El objeto de comparación siempre debe igualarse a un build optimizado equivalente a producción.
2. No resolver el mismo problema
El formato de entrada es distinto, la salida es distinta, solo una parte tiene manejo de errores, la política de reutilización de memoria es distinta. Si se deja pasar esto, se acaba midiendo no la velocidad, sino una diferencia de requisitos.
3. Sacar una conclusión con una sola ejecución
Una sola ejecución es, casi siempre, ruido.
- JIT
- caché de páginas
- boost de la CPU
- calor
- tareas en segundo plano
- GC
- la primera lectura de archivo
Todo esto se mezcla de golpe en una sola ejecución.
4. Mezclar el warm-up
Al medir C# y Java, si queda ambiguo si se incluye la primera ejecución o si solo se mira después del warm-up, el argumento se derrumba. El cold y el warm se tratan como cosas distintas.
5. No verificar la corrección
Un benchmark necesita, antes que “es rápido”, que “devuelve el mismo resultado”. Hay que confirmar siempre que todas las implementaciones comparadas obtienen el mismo checksum o la misma salida a partir de la misma entrada.
6. Decidir la visión de conjunto con un solo microbenchmark
Ganar solo en un tight loop no implica ganar en todo el servicio real. Al contrario, perder en tiempo de arranque no impide ser suficientemente fuerte en funcionamiento prolongado.
Enfoque básico al comparar C#, C++, Java y Go
Este punto es bastante importante. Lo que se recomienda es una estructura de dos capas.
flowchart TB
accTitle: Estructura de dos capas para el benchmark entre lenguajes
accDescr: La capa externa es un runner común que aleatoriza el orden de ejecución, separa cold y warm, verifica el checksum y guarda los datos en bruto, e invoca el ejecutable de bench de cada lenguaje (C++, C#, Java, Go). Cada ejecutable, a su vez, se puede medir en detalle dentro de su propio lenguaje con la capa interna: Google Benchmark, BenchmarkDotNet, JMH, o go test -bench y benchstat.
subgraph outer["Capa externa: runner común entre lenguajes"]
direction TB
R["Runner común<br/>Aleatorización del orden de ejecución / separación de cold y warm<br/>Verificación de checksum / guardado de datos en bruto"]
R --> E1["Ejecutable de bench<br/>C++"]
R --> E2["Ejecutable de bench<br/>C#"]
R --> E3["Ejecutable de bench<br/>Java"]
R --> E4["Ejecutable de bench<br/>Go"]
end
subgraph inner["Capa interna: profundización dentro de cada lenguaje"]
direction TB
H1["Google Benchmark"]
H2["BenchmarkDotNet"]
H3["JMH"]
H4["go test -bench y benchstat"]
end
E1 -.->|"medir el mismo procesamiento en detalle dentro del lenguaje"| H1
E2 -.->|"medir el mismo procesamiento en detalle dentro del lenguaje"| H2
E3 -.->|"medir el mismo procesamiento en detalle dentro del lenguaje"| H3
E4 -.->|"medir el mismo procesamiento en detalle dentro del lenguaje"| H4
Las cifras que salen de la capa externa son cifras que se pueden comparar entre lenguajes, y las que salen de la capa interna son cifras para seguir mejoras dentro de ese lenguaje. El punto clave es no mezclar estas dos cosas en una sola tabla.
1. Para la medición dentro de un lenguaje, usar el harness adecuado a ese lenguaje
Cada lenguaje tiene herramientas de benchmark que absorben las particularidades de ese lenguaje.
- C#: BenchmarkDotNet
- Java: JMH
- Go:
go test -benchybenchstat - C++: Google Benchmark
Estas herramientas se encargan, hasta cierto punto, de las particularidades del runtime, del procesamiento estadístico y de las trampas de medición. Son bastante útiles para la comparación dentro de un mismo lenguaje y para profundizar en una implementación.
Por ejemplo, si se mide “ordenar 10 millones de int32” con BenchmarkDotNet, la configuración mínima queda así.
// C# / .NET 8 + BenchmarkDotNet 0.13.x
// dotnet add package BenchmarkDotNet
// dotnet run -c Release
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<SortBench>();
[MemoryDiagnoser] // También muestra la cantidad asignada y el número de GC
public class SortBench
{
private int[] _source = Array.Empty<int>();
private int[] _work = Array.Empty<int>();
[GlobalSetup]
public void Setup()
{
var rng = new Random(12345); // Fija la seed para tener siempre la misma entrada
_source = new int[10_000_000];
for (int i = 0; i < _source.Length; i++)
{
_source[i] = rng.Next();
}
_work = new int[_source.Length];
}
[IterationSetup] // Devuelve al estado sin ordenar cada vez
public void ResetInput() => Array.Copy(_source, _work, _source.Length);
[Benchmark]
public long SortInt32()
{
Array.Sort(_work);
long checksum = 0;
foreach (int v in _work)
{
checksum = checksum * 31 + v;
}
return checksum; // Devuelve el resultado para que la optimización no lo elimine
}
}
[IterationSetup] tiene una restricción. La documentación oficial de BenchmarkDotNet no lo recomienda en microbenchmarks porque ensucia el resultado, y afirma que es útil en un macrobenchmark que tarde 100 ms o más. Ordenar 10 millones de elementos cumple esa condición, pero al medir procesamientos cortos conviene pasar la entrada por el lado de [GlobalSetup].
En JMH de Java, hay que pensar en lo mismo.
// Java / JMH. En pom.xml se añaden jmh-core y jmh-generator-annprocess
// mvn clean verify
// java -jar target/benchmarks.jar SortBench
package bench;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3) // Separa la JVM para promediar los aciertos y errores del JIT
public class SortBench {
private int[] source;
private int[] work;
@Setup(Level.Trial)
public void setUp() {
Random rng = new Random(12345); // Fija la seed para tener siempre la misma entrada
source = new int[10_000_000];
for (int i = 0; i < source.length; i++) {
source[i] = rng.nextInt();
}
work = new int[source.length];
}
@Setup(Level.Invocation) // Devuelve al estado sin ordenar cada vez
public void resetInput() {
System.arraycopy(source, 0, work, 0, source.length);
}
@Benchmark
public long sortInt32() {
Arrays.sort(work);
long checksum = 0;
for (int v : work) {
checksum = checksum * 31 + v;
}
return checksum; // El valor de retorno lo consume JMH, así que no lo elimina la optimización
}
}
Level.Invocation tiene el mismo tipo de restricción. El javadoc de JMH deja escrito que este nivel solo se puede usar en benchmarks donde una sola llamada al método @Benchmark supere 1 milisegundo. Como se toma una marca de tiempo en cada llamada, en procesamientos cortos la propia medición se convierte en el cuello de botella.
Los valores de @Warmup / @Measurement / @Fork de BenchmarkDotNet y JMH son, tal cual, condiciones del experimento. Hay que dejarlos siempre registrados junto con el resultado.
2. Para la comparación entre lenguajes, poner un runner común en la capa externa
Por otro lado, poner en paralelo, tal cual, el resultado de BenchmarkDotNet de C# y el resultado de JMH de Java es algo arriesgado. La razón es que las convenciones de cada harness son distintas.
Por eso, para la comparación entre lenguajes conviene convertir cada implementación en un ejecutable invocable con el mismo contrato de CLI, y ejecutarlo desde fuera en las mismas condiciones.
Por ejemplo, en cada lenguaje se prepara un ejecutable con esta forma.
bench --scenario sort_int32 --dataset data/sort_10m.bin --mode warm
bench --scenario group_words --dataset data/words_100mb.txt --mode cold
bench --scenario parallel_hash --dataset data/blob_1gb.bin --threads 8
También conviene establecer un contrato para la salida. Si se acuerda que la implementación de cualquier lenguaje saque por la salida estándar solo estas 2 líneas, el análisis del lado del runner se resuelve con una sola expresión regular por línea.
checksum=[cadena en hexadecimal]
inner_ms=[milisegundos en decimal]
checksum sirve para verificar la corrección, e inner_ms es el tiempo del procesamiento principal medido por la propia implementación. Como el runner mide por separado el wall-clock de todo el proceso, quedan registrados tanto el tiempo incluyendo el arranque como el tiempo solo del cuerpo del procesamiento.
Y en el runner común se sigue este flujo:
- aleatorizar el orden de ejecución
- separar cold / warm
- pasar el mismo conjunto de datos
- verificar el checksum
- tomar el wall-clock y la memoria
- guardar los datos en bruto en CSV / JSON
Si solo se escribe el esqueleto, queda así.
# run-bench.ps1 : runner común entre lenguajes (esqueleto)
# Funciona con PowerShell 7.4.
# pwsh ./run-bench.ps1 -Scenario sort_int32 -Dataset ./data/sort_10m.bin -Runs 15
param(
[Parameter(Mandatory = $true)][string]$Scenario,
[Parameter(Mandatory = $true)][string]$Dataset,
[int]$Runs = 15,
[int]$WarmupRuns = 3,
[string]$OutCsv = "./results/raw.csv"
)
# Ejecutables de cada lenguaje. El contrato de CLI se iguala por completo
# entre las 4 implementaciones.
# Para Java se coloca un wrapper fino que solo envuelve java -jar, para unificar la forma de invocarlo.
$Implementations = @(
@{ Language = "cpp"; Exe = "./build/cpp/bench.exe" },
@{ Language = "csharp"; Exe = "./build/csharp/bench.exe" },
@{ Language = "java"; Exe = "./build/java/bench.cmd" },
@{ Language = "go"; Exe = "./build/go/bench.exe" }
)
function Invoke-OneRun {
param(
[Parameter(Mandatory = $true)][hashtable]$Impl,
[Parameter(Mandatory = $true)][string]$Mode,
[Parameter(Mandatory = $true)][int]$Index
)
# $Scenario y $Dataset son los definidos en el bloque param del principio
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$stdout = & $Impl.Exe --scenario $Scenario --dataset $Dataset --mode $Mode
$sw.Stop()
$exitCode = $LASTEXITCODE
# El contrato de salida con la implementación se analiza aquí, en un único lugar
$checksum = ""
$innerMs = [double]::NaN
foreach ($line in $stdout) {
if ($line -match '^checksum=(\S+)$') { $checksum = $Matches[1] }
if ($line -match '^inner_ms=(\S+)$') { $innerMs = [double]$Matches[1] }
}
if ($exitCode -ne 0 -or [string]::IsNullOrEmpty($checksum)) {
throw "$($Impl.Language) / $Mode / run $Index ha fallado. exit=$exitCode"
}
# Una implementación que olvida emitir inner_ms devuelve solo el checksum con
# código de salida 0. Si no se detecta aquí, queda como NaN en el CSV y la
# comparación del tiempo interno se rompe en silencio
if ([double]::IsNaN($innerMs) -or [double]::IsInfinity($innerMs) -or $innerMs -lt 0) {
throw "$($Impl.Language) / $Mode / run $Index no ha devuelto inner_ms (valor: $innerMs). " +
"El contrato de salida son las 2 líneas checksum= e inner_ms="
}
return [pscustomobject]@{
timestamp = (Get-Date).ToString("o")
language = $Impl.Language
scenario = $Scenario
cold_or_warm = $Mode
run_index = $Index
process_ms = [math]::Round($sw.Elapsed.TotalMilliseconds, 3)
inner_ms = $innerMs
checksum = $checksum
}
}
# 1. Primero, verificación de corrección. Si no todas las implementaciones
# devuelven el mismo checksum, no tiene sentido medir la velocidad
$expected = $null
foreach ($impl in $Implementations) {
for ($i = 1; $i -le $WarmupRuns; $i++) {
$r = Invoke-OneRun -Impl $impl -Mode "warm" -Index $i
if ($null -eq $expected) {
$expected = $r.checksum
}
elseif ($r.checksum -ne $expected) {
throw "Los checksum no coinciden. $($impl.Language) da $($r.checksum), la referencia es $expected"
}
}
}
# 2. Producción. En cada run se baraja el orden de ejecución para repartir
# el sesgo de calor y de franja horaria
# La verificación del punto 1 se hace sobre runs de warm-up que se descartan,
# así que los runs que sí se registran también deben verificar el checksum
# uno por uno. Si se omite esto, se cuelan en el CSV tiempos de una
# implementación que, a partir de cierto punto, empezó a hacer otro trabajo,
# y eso invalida la comparación entera
$rows = [System.Collections.Generic.List[object]]::new()
foreach ($mode in @("cold", "warm")) {
for ($i = 1; $i -le $Runs; $i++) {
$shuffled = $Implementations | Get-Random -Count $Implementations.Count
foreach ($impl in $shuffled) {
$r = Invoke-OneRun -Impl $impl -Mode $mode -Index $i
if ($r.checksum -ne $expected) {
throw "Los checksum no coinciden. El run $mode #$i de $($impl.Language) da $($r.checksum), la referencia es $expected"
}
$rows.Add($r)
}
}
}
# 3. Siempre se guardan los datos en bruto. La agregación se hace después a partir de este CSV
# Si se pasa un nombre sin directorio padre, como -OutCsv raw.csv,
# Split-Path -Parent devuelve una cadena vacía y New-Item la rechaza.
# Como esto falla después de terminar todos los runs, se pierde por completo el resultado de la medición
$outDir = Split-Path -Parent $OutCsv
if ($outDir) { New-Item -ItemType Directory -Force -Path $outDir | Out-Null }
$rows | Export-Csv -Path $OutCsv -NoTypeInformation -Encoding utf8
Write-Host "raw data: $OutCsv / $($rows.Count) filas"
Aquí se hacen 3 cosas de forma deliberada.
- Poner la verificación de corrección antes de la medición de velocidad. Comparar solo la velocidad con los checksum desalineados no tiene sentido.
- Barajar en cada run. El objetivo es evitar el orden de “primero se ejecuta todo A y después todo B”.
- Escribir solo los datos en bruto, sin agregar. La media y la mediana se calculan después sobre el CSV.
Hacer esto facilita separar las buenas prácticas dentro de cada lenguaje de la equidad entre lenguajes.
Ejemplo concreto: qué elementos de benchmark preparar
Cuando se pide “quiero comparar C#, C++, Java y Go”, si es un solo benchmark, lo recomendable es un cálculo de CPU sencillo y difícil de malinterpretar; si son varios, conviene preparar de 3 a 4 workloads de carácter distinto.
Configuración recomendada
1. sort_int32_10m
Objetivo: ver el uso de CPU, ancho de banda de memoria y área temporal
- Entrada: 10 millones de
int32generados con una seed fija - Procesamiento: ordenar el array y devolver el checksum
- Punto a cuidar: volver siempre a la misma entrada sin ordenar
Este es relativamente fácil de interpretar. Sin embargo, como también incluye la diferencia de la implementación de ordenación estándar, es más una comparación que incluye la biblioteca estándar que una comparación del propio lenguaje.
2. hash_group_count
Objetivo: ver la tendencia de tablas hash, procesamiento de cadenas, asignación y GC
- Entrada: datos de texto fijos
- Procesamiento: contar el número de apariciones de cada palabra
- Salida: los N primeros elementos y el checksum
Esto está más cerca del uso real, aunque también pesan bastante las diferencias de la biblioteca de cadenas y de la implementación de map. Por eso mismo, es una comparación más cercana a la realidad.
3. parallel_sha256
Objetivo: ver el procesamiento paralelo, el planificador, el pool de workers y las particularidades de la sincronización
- Entrada: una secuencia de chunks binarios de tamaño fijo
- Procesamiento: aplicar hash en orden con N hilos y devolver el checksum final
- Condición: escalonar el número de hilos, por ejemplo 1 / 2 / 4 / 8
Aquí se ve más fácilmente cómo escala en ejecución paralela que en un tight loop simple.
4. startup_noop o startup_parse_small
Objetivo: ver el tiempo de arranque
noop: arranca y termina enseguidaparse_small: procesa una entrada pequeña una sola vez y termina
Aquí se nota fácilmente el coste del JIT y de la inicialización de C#/Java, y difiere bastante de cómo se ven C++/Go. Dicho de otro modo, aunque aparezca una diferencia aquí, es algo distinto del resultado en procesamiento de larga duración.
¿Qué hacer con los benchmarks de JSON o HTTP?
JSON y HTTP están cerca del uso real, así que por supuesto tienen sentido. Sin embargo, en ese caso pasa a ser más una comparación que incluye la biblioteca, el framework y el ecosistema, que una comparación de lenguajes.
Eso en sí mismo no es malo. De hecho, en la práctica esa parte suele ser más importante. Pero en un artículo o informe conviene dejar escrito con claridad:
Esto no es una comparación de lenguajes, sino una comparación que incluye una implementación estándar y las bibliotecas principales
para que haya menos malentendidos.
Condiciones que hay que igualar según el lenguaje
C++
- igualar al build optimizado
- fijar el compilador
- fijar la implementación de la biblioteca estándar
- dejar por escrito condiciones como
-O3//O2, LTO, PGO - vigilar que el resultado no desaparezca por la optimización
- sospechar si la velocidad viene de un comportamiento indefinido
C++ tiene mucha libertad, así que la diferencia de condiciones se nota mucho más. Por eso es bastante importante con qué compilador, con qué flags y con qué STL se midió.
C#
- igualar al build de Release
- fijar la versión de .NET
- registrar condiciones como Server GC / Workstation GC
- dejar constancia de si hay Tiered Compilation, ReadyToRun o Native AOT
- separar cold y warm
En C# las diferencias de configuración de .NET cambian el resultado.
En particular, el C# con JIT y el C# con Native AOT son, aunque ambos se llamen “C#”, ejes distintos.
Mezclar esto convierte el objeto de comparación, no en el lenguaje, sino en la forma de distribución.
Java
- fijar el vendor y la versión del JDK
- dejar escrito el GC
- fijar warm-up / measurement / fork
- registrar el tamaño del heap y las opciones de la JVM
- separar el cold start del steady-state
Java se beneficia con facilidad del JIT, pero en contrapartida el resultado de la primera ejecución cambia bastante. Por eso es imprescindible separar la comparación de procesos de vida corta de la comparación en funcionamiento prolongado.
Go
- fijar la versión de Go
- fijar
GOMAXPROCS - dejar escrito
CGO_ENABLED - si se toca
GOGC, registrarlo siempre - si es posible, guardar la salida en formato benchmark
Go es relativamente fácil de manejar, pero en benchmarks paralelos la influencia de GOMAXPROCS es grande.
Además, usar cgo o no cambia el panorama por completo, así que eso también hay que dejarlo siempre como condición registrada.
Cómo igualar el entorno de ejecución
Sea cual sea el lenguaje, una comparación sin el entorno igualado, en general, acaba comparando el entorno.
Lo que hay que igualar
- misma CPU / memoria / almacenamiento
- misma versión del sistema operativo
- mismas condiciones de energía
- condiciones lo más parecidas posible de temperatura ambiente
- mismos datos de entrada
- misma prioridad de proceso
- misma condición de número de núcleos
- misma condición de contenedor o bare metal
Lo que pesa especialmente
Configuración de energía y frecuencia de la CPU
En un portátil, basta con estar conectado a corriente o funcionando con batería para que sea otro mundo completamente distinto. Si el governor de la CPU o el modo de energía no están igualados, el resultado de la comparación fluctúa bastante.
Sobre cómo igualar en Windows las condiciones de energía, las notificaciones, el ruido de fondo, el calor y el orden de ejecución, lo tratamos en detalle en otro artículo: Cómo comparar la velocidad de ejecución de distintas versiones de un programa en Windows. Si se mide en Windows, esto pesa bastante.
Calor
Si solo las primeras ejecuciones son rápidas y luego baja el rendimiento, hay que sospechar del calor o del throttling. En lugar de ejecutar todo A y después todo B, alternar como A / B / A / B reduce el sesgo.
Procesos en segundo plano
Actualizaciones, indexación, sincronización, antivirus, navegador, herramientas de chat. Todo esto es discreto, pero afecta con normalidad.
Qué se debe medir
En la comparación de lenguajes, se recomienda separar como mínimo estos 4 aspectos.
1. wall-clock time
Es el tiempo real que espera el usuario. Es el indicador que primero hay que mirar.
2. CPU time
Es “cuánta CPU se usó realmente”. Si solo el wall-clock es rápido pero el CPU time no cambia, puede deberse a la influencia de la espera o de E/S.
3. memoria / asignaciones
- RSS máximo
- cantidad total asignada
- número de asignaciones (alloc)
- número de GC
- pausas de GC
Mirar esto permite ver el coste que hay detrás de la velocidad.
4. distribución
- mediana
- p95 / p99
- min / max
- desviación estándar o dispersión
Hablar solo con la media no deja ver la verdadera cara del procesamiento que, de vez en cuando, se dispara.
Procedimiento de ejecución recomendado
El flujo que resulta fácil de operar en la práctica es, más o menos, este orden.
flowchart TB
accTitle: Procedimiento recomendado para ejecutar un benchmark entre lenguajes
accDescr: Diagrama de flujo que va desde decidir el workload y fijar el conjunto de datos, pasando por la verificación de corrección, la fijación de las condiciones de build, la separación de cold y warm, la ejecución aleatorizada, el guardado de los datos en bruto, hasta decidir si hubo una diferencia significativa y, en ese caso, tomar un profile para indagar la causa.
a1["1. Decidir el workload"] --> a2["2. Fijar el conjunto de datos común"]
a2 --> a3["3. Pasar primero la verificación de corrección"]
a3 --> a4{"¿Coincide el checksum<br/>de todas las implementaciones?"}
a4 -- No --> a3
a4 -- Sí --> a5["4. Fijar las condiciones de build"]
a5 --> a6["5. Separar cold y warm"]
a6 --> a7["6. Ejecutar aleatorizando el orden"]
a7 --> a8{"¿Se alcanzó el número<br/>de veces necesario?"}
a8 -- No --> a7
a8 -- Sí --> a9["8. Guardar los datos en bruto"]
a9 --> a10{"¿Apareció una diferencia<br/>significativa?"}
a10 -- No --> a11["Registrar condiciones y número de repeticiones, y terminar"]
a10 -- Sí --> a12["9. Tomar un profile para indagar la causa"]
1. Decidir el workload
Primero, dejar claro qué es lo que se quiere comparar.
- tiempo de arranque
- throughput en régimen estable
- tail latency
- eficiencia de memoria
- escalado paralelo
2. Fijar el conjunto de datos común
Los datos de entrada se igualan con una seed fija o con un archivo fijo. Si también se incluye la generación de datos, esa parte también debe hacerse en las mismas condiciones en cada lenguaje.
3. Pasar primero la verificación de corrección
Con datos pequeños y con datos grandes, se confirma que todas las implementaciones devuelven el mismo resultado. Es cómodo hacer que emitan un checksum o un hash.
4. Fijar las condiciones de build
En cada lenguaje se genera un ejecutable de Release/optimizado, y se registra la versión y los flags.
5. Separar cold y warm
Esto es especialmente importante en C# y Java.
- cold: incluye justo después del arranque del proceso
- warm: el estado estable tras varias ejecuciones
Si se dibuja hasta dónde se está midiendo, queda claro que estas dos cosas son distintas.
flowchart LR
accTitle: Rango que abarcan las mediciones cold y warm
accDescr: El rango cold abarca el arranque del proceso, la inicialización del runtime y la carga de clases, la primera compilación del JIT y la primera ejecución del cuerpo del procesamiento. A partir de ahí, las ejecuciones siguientes avanzan la Tiered Compilation hasta llegar al procesamiento principal en estado estable, que es el rango medido como warm.
subgraph coldrange["Rango medido como cold"]
direction LR
s1["Arranque del proceso"] --> s2["Inicialización del runtime<br/>carga de clases"]
s2 --> s3["Primera compilación del JIT"] --> s4["Primera ejecución del cuerpo del procesamiento"]
end
s4 --> s5["Ejecuciones siguientes del cuerpo del procesamiento<br/>avanza la Tiered Compilation"]
subgraph warmrange["Rango medido como warm"]
direction LR
s6["Procesamiento principal en estado estable"]
end
s5 --> s6
Como C++ y Go están precompilados de antemano, no tienen un proceso equivalente a “primera compilación del JIT”, y la inicialización del runtime también resulta relativamente ligera. Buena parte de la diferencia del cold nace aquí. Precisamente por eso, es mejor no mezclar estos dos lenguajes en la misma tabla.
6. Alternar o aleatorizar el orden de ejecución
Ejemplo:
cpp -> csharp -> java -> go
go -> java -> cpp -> csharp
csharp -> go -> java -> cpp
...
Así se reduce el sesgo de calor y de ruido.
7. Asegurar el número de repeticiones
Para un microbenchmark ligero conviene bastante más repeticiones; para uno end-to-end se quieren al menos 10 o más. Si la diferencia es pequeña y el número de repeticiones es bajo, la interpretación se vuelve bastante insegura.
8. Guardar los datos en bruto
No basta con el resultado agregado: se conservan también los datos en bruto de cada run. Al revisarlos después se pueden leer los valores atípicos y las particularidades del warm-up.
9. Si aparece una diferencia, tomar un profile
Solo cuando aparece una diferencia se empieza a indagar en la causa.
- profile de CPU
- profile de asignaciones
- log de GC
- flame graph
- trazas del lado del sistema operativo
Llegados a este punto, se puede hablar no de “rápido / lento”, sino de por qué ocurre así.
Cómo interpretar los resultados
Incluso después de tener las cifras, si se interpreta mal, sigue siendo peligroso.
C# / Java son lentos solo en la primera ejecución
Hay que sospechar de la influencia del JIT, la carga de clases y la inicialización. En este caso,
- si el tiempo de arranque es importante, es una diferencia con sentido
- si el tema principal es el funcionamiento prolongado, es una diferencia que debería separarse en otra tabla
C++ es fuerte en el tight loop
Puede deberse a la optimización de bajo nivel, la disposición de los objetos y el overhead mínimo del runtime. Sin embargo, mirar solo eso y decir “por tanto es lo más rápido también en el servicio real” es un salto excesivo.
Go parece tener ventaja en tiempo de arranque y en facilidad de distribución
Puede pesar el binario único, un arranque relativamente ligero y un modelo de paralelismo fácil de manejar. Sin embargo, no está garantizado que tenga ventaja en todos los workloads de CPU.
C# / Java se acercan bastante en steady-state, o incluso invierten el resultado
Puede deberse a que la optimización del JIT está funcionando. Tampoco es un caso raro. Por eso es importante no mezclar la comparación que incluye el arranque con la comparación en estado estable.
En procesamiento intensivo en asignaciones la diferencia es grande
En este caso, más que el nombre del lenguaje, suele pesar más
- el layout de memoria
- el manejo de cadenas y map
- el comportamiento del GC
- las copias de más
Plantilla de registro
Conviene dejar registrados, como mínimo, estos elementos en el resultado del benchmark; ayuda bastante más adelante.
timestamp,language,scenario,run_kind,cold_or_warm,elapsed_ms,cpu_ms,max_rss_mb,alloc_bytes,gc_count,checksum
compiler_or_runtime,compiler_version,flags,os,cpu,threads,input_id,notes
Por ejemplo, run_kind se puede separar así.
micromacrostartupparallel
En cold_or_warm conviene dejar siempre claro cuál de los dos es.
coldwarm
Decidir de antemano qué se pone en cada columna evita que haya inconsistencias.
| Columna | Qué contiene | Ejemplo de formato |
|---|---|---|
timestamp |
Hora de inicio del run. Se usa para revisar después la variación según la franja horaria | Formato ISO 8601. 2026-03-17T10:00:00+09:00 |
language |
Identificador de la implementación. Se fija un vocabulario cerrado para evitar variaciones de escritura | cpp / csharp / java / go |
scenario |
Nombre del elemento de benchmark | sort_int32_10m |
run_kind |
Tipo de medición | micro / macro / startup / parallel |
cold_or_warm |
Si incluye el arranque o no | cold / warm |
elapsed_ms |
wall-clock. Conviene dejar hasta el tercer decimal para no tener problemas después | milisegundos en decimal |
cpu_ms |
Tiempo de CPU del proceso. Valor de sumar user y system | milisegundos en decimal |
max_rss_mb |
RSS máximo | MB entero o decimal |
alloc_bytes |
Bytes totales asignados. En un lenguaje donde no se pueda obtener, se deja en blanco, y esa propia casilla en blanco queda registrada | entero, o vacío |
gc_count |
Número de GC. En C++ siempre vacío | entero, o vacío |
checksum |
Para verificación de corrección. Se comprueba aparte que coincide en todas las implementaciones | cadena en hexadecimal |
compiler_or_runtime |
Tipo de procesador | msvc / dotnet / temurin / go |
compiler_version |
Versión del procesador. Se anota hasta la versión menor | cadena de versión que emite el propio procesador |
flags |
Condiciones de optimización. /O2, -O3 -flto, Server GC, GOMAXPROCS=8, etc. |
cadena separada por espacios |
os / cpu / threads |
Entorno de ejecución | nombre del SO y número de build, modelo de CPU, número de hilos usados |
input_id |
Identificador del conjunto de datos. Usar el hash del archivo es lo más fiable | nombre de archivo y hash |
notes |
Notas sobre runs con alguna anomalía | texto libre |
Lo importante es permitir columnas vacías en las columnas de valores medidos, y dejar registrado el hecho mismo de que están vacías. “En C++ no existe gc_count” es información, pero si se elimina la columna entera, luego no se puede saber.
Qué se pierde de vista si solo se mira la media
Al agregar, si se resume todo en una sola media, se pierde información. Lo que sigue es una cifra ficticia solo para explicar la aritmética; no corresponde a la medición real de ningún lenguaje. Supongamos que son los elapsed_ms de 10 runs de una implementación, ordenados de más rápido a más lento.
98, 99, 100, 101, 101, 102, 103, 104, 106, 720
Los valores representativos que salen de estos 10 números quedan así.
| Indicador | Valor | Lectura |
|---|---|---|
| Media | 163,4 | Arrastrada por la última ejecución, se desvía cerca de un 60 % por encima del rango habitual real |
| Mediana | 101,5 | Un valor cercano a la sensación de 9 de cada 10 ejecuciones |
| min / max | 98 / 720 | Con una diferencia de más de 7 veces, es la señal para investigar primero la causa del valor atípico |
| p95 / p99 | No se puede calcular | Con 10 muestras, ni el percentil 95 ni el 99 tienen sentido |
Es decir, una tabla que solo lleva la media le da la misma cara a “una implementación que de vez en cuando se detiene mucho tiempo” y a “una implementación estable pero algo más lenta”. En cuanto a cómo construir la tabla, pesa la siguiente diferencia.
| Punto de vista | Tabla de resultados débil | Tabla de resultados útil |
|---|---|---|
| Valor representativo | Solo la media | La mediana como protagonista, junto con min / max y la dispersión |
| Número de intentos | No está escrito | Se indica el número de runs y la política sobre si se excluyeron valores atípicos |
| cold / warm | Mezclados, o sin distinción | Tabla separada, o al menos fila separada |
| Corrección | No se menciona | Se deja escrito que el checksum coincidió en todas las implementaciones |
| Condiciones | “Medido en el mismo PC” | Se registra hasta el sistema operativo, la CPU, la versión del procesador, los flags de optimización y el número de hilos |
| Datos en bruto | Solo los valores agregados | Se indica dónde se guardó el CSV en bruto |
Si se quiere incluir p95 o p99, hace falta antes que nada aumentar el número de runs. Para hablar de una distribución, hace falta un número de muestras en el que la distribución se pueda ver; no es más que eso.
Un benchmark, muchas veces, importa más poder interpretarlo después que el propio hecho de medirlo.
Resumen
Lo verdaderamente importante en la comparación de velocidad entre C#, C++, Java y Go es convertir la pregunta imprecisa de qué lenguaje es el más rápido en la forma experimental de qué workload, en qué condiciones y con qué indicador se compara.
En particular, hay puntos difíciles de pasar por alto:
- separar el tiempo de arranque del estado estable
- medir con el mismo algoritmo, la misma entrada y la misma verificación de corrección
- no sacar una conclusión con un solo benchmark
- separar el benchmark dentro del lenguaje del benchmark entre lenguajes
- mirar la mediana y la distribución más que la media
- dejar registradas las condiciones y los datos en bruto
Y lo más importante al final es no intentar decidir demasiado el ganador por el nombre del lenguaje. El rendimiento real se decide por la combinación de lenguaje, runtime, bibliotecas, condiciones de build, datos, sistema operativo y hardware.
Frases como “C++ es rápido”, “Java es fuerte”, “Go es ligero” o “C# también es suficientemente rápido” son, en cierto sentido, todas ciertas. Pero si falta en qué condiciones se dice eso, la discusión termina sin llegar a encajar.
Igualar las condiciones, usar varios workloads, separar cold / warm y mirar hasta la distribución. Es un trabajo discreto, pero al final es lo más sólido.
Referencias
-
BenchmarkDotNet Getting Started https://benchmarkdotnet.org/articles/guides/getting-started.html
-
BenchmarkDotNet Setup and Cleanup (condiciones en las que se puede usar
[IterationSetup]) https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html -
OpenJDK JMH Project https://openjdk.org/projects/code-tools/jmh/
-
javadoc de
Levelde JMH (restricciones y advertencias deLevel.Invocation) https://javadoc.io/doc/org.openjdk.jmh/jmh-core/latest/org/openjdk/jmh/annotations/Level.html -
Repositorio GitHub / README de JMH https://github.com/openjdk/jmh
-
Paquete
testingde Go https://pkg.go.dev/testing -
benchstatde Go https://pkg.go.dev/golang.org/x/perf/cmd/benchstat -
Google Benchmark User Guide https://google.github.io/benchmark/user_guide.html
-
Cómo comparar la velocidad de ejecución de distintas versiones de un programa en Windows https://comcomponent.com/es/blog/windows-benchmark-comparing-program-versions/
Artículos relacionados
Páginas que ayudan a entender mejor este artículo si se leen junto con él.
Áreas de consultoría relacionadas
El diseño de comparaciones de rendimiento, la forma de igualar las condiciones de medición, la interpretación de resultados y la investigación de causas encajan bien con estos servicios.
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Compatibilidad retroactiva de interfaces DLL y COM — Tabla de decisión sobre qué cambios rompen al lado que llama
Qué cambios de DLL o COM rompen al lado que llama: los tres niveles de compatibilidad, la tabla de decisión por cambio y la regla de inmu...
Lista de verificación para manejar procesos secundarios de forma segura en aplicaciones de Windows
Para manejar procesos secundarios de forma segura en aplicaciones de Windows, la propiedad del árbol de procesos y el procedimiento de ci...
Trampas de la memoria compartida y buenas prácticas para producción
Analizamos las trampas de usar memoria compartida en producción y el diseño que reduce la tasa de incidentes: sincronización, visibilidad...
Cómo invocar una DLL nativa de C# Native AOT desde C/C++
Publicar una biblioteca de C# como DLL nativa con Native AOT e invocar sus puntos UnmanagedCallersOnly desde C/C++: casos de uso, patrone...
WPR/WPA en la práctica — Introducción al análisis de rendimiento del sistema para «todo el PC va lento»
Problemas como «todo el PC va lento» o «el arranque es lento», que el Administrador de tareas no explica, se investigan con WPR/WPA leyen...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Consultoría técnica y revisión de diseño
El diseño de comparaciones de rendimiento, la forma de igualar las condiciones de medición y la lectura del warm-up y las estadísticas encajan bien con la consultoría técnica y la revisión de diseño.
Investigación de fallos y causas
Aislar la causa de diferencias de rendimiento entre lenguajes o versiones, identificar cuellos de botella y verificar la validez del procedimiento de medición se abordan bien como investigación de fallos y análisis de causas.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Cuál de C#, C++, Java y Go es el más rápido?
- No se puede decidir con una sola cifra. El rendimiento real depende de la combinación de lenguaje, runtime, bibliotecas, condiciones de compilación, datos, sistema operativo y hardware. Lo importante es convertir la pregunta "¿qué lenguaje es el más rápido?" en una forma experimental: "qué workload, en qué condiciones y con qué indicador se compara". En el tiempo de arranque suele notarse el coste del JIT y la inicialización de C#/Java, mientras que en steady-state no es raro que las optimizaciones del JIT reduzcan bastante la diferencia, o incluso la inviertan.
- ¿Por qué se separa el warm-up en los benchmarks de C# y Java?
- Porque C# y Java suelen recibir la influencia del JIT, así que medir la primera ejecución no mide solo la velocidad del propio programa, sino también el arranque del runtime, la carga de clases y la preparación del JIT. C++ y Go, en cambio, suelen estar precompilados. Tanto cold como warm tienen sentido, pero no es el mismo sentido: por eso conviene tratar por separado el cold, que incluye el arranque del proceso, y el warm, que es el estado estable tras varias ejecuciones, y no mezclarlos en la misma tabla.
- ¿Cómo se debería diseñar un benchmark que cruce varios lenguajes?
- Se recomienda una estructura de dos capas. Para la medición dentro de cada lenguaje se usa el harness adecuado a ese lenguaje: BenchmarkDotNet (C#), JMH (Java), go test -bench junto con benchstat (Go), o Google Benchmark (C++). Para la comparación entre lenguajes es arriesgado poner en paralelo, tal cual, los resultados de cada harness, así que conviene convertir cada implementación en un ejecutable que se invoque con el mismo contrato de CLI, y hacer que un runner común externo se encargue de aleatorizar el orden de ejecución, separar cold y warm, usar el mismo conjunto de datos, verificar el checksum y guardar los datos en bruto.
- ¿En qué hay que fijarse en un microbenchmark de C++?
- Hay que tener cuidado con la trampa de que la optimización elimine el procesamiento. Si el compilador decide que "nadie usa este resultado de cálculo", puede eliminar el propio procesamiento, y entonces el resultado no es que sea rápido, sino que en realidad no está haciendo nada. Por eso es importante usar el resultado, emitir un checksum, y apoyarse en las funciones de los frameworks de benchmark para impedir esa optimización. Además, en C++ las diferencias de condiciones se notan mucho más, así que es bastante importante dejar por escrito con qué compilador, con qué flags (-O3/O2, LTO, PGO, etc.) y con qué STL se midió.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.