El Setup de Tests

Para que estos benchmarks tengan sentido, los detalles del hardware importan:

  • Baseline x86: m7i.xlarge, 4 vCPUs (Intel Sapphire Rapids 4ta Gen), 16GB RAM, EBS gp3
  • Target ARM64: r8g.xlarge, 4 vCPUs (Graviton4, Neoverse V2), 32GB RAM, EBS gp3
  • Región: us-east-1
  • OS: Amazon Linux 2023, kernel 6.1
  • Testing: Cada benchmark corrió 10 veces, los números son mediana con bounds p5/p95

Sí, el r8g tiene más RAM. Las familias de instancias deben elegirse según las necesidades reales del workload, no por simetría de benchmarks sintéticos. En producción, se elige la instancia que le queda a la aplicación, no la que hace un gráfico de comparación justo.

Node.js (v22.x): El Punto Medio Sorprendente

Un servicio Node.js representativo para benchmarking es un API gateway, Express.js, mucho parsing de JSON, validación de JWT, rate limiting, transformación de respuestas. Clásico I/O-bound con ráfagas de CPU para crypto y JSON.

Throughput HTTP (wrk, 100 conexiones concurrentes, 60s)

Métrica m7i.xlarge (x86) r8g.xlarge (ARM64) Delta
Requests/seg 14,230 15,870 +11.5%
Latencia promedio 6.89ms 6.21ms -9.9%
Latencia p99 18.4ms 14.7ms -20.1%
Uso CPU (promedio) 72% 64% -11.1%

La mejora en p99 es el resultado más notable. El rendimiento más predecible de los cores de Graviton4 (sin variación de turbo boost) te da distribuciones de latencia más ajustadas. En una arquitectura de microservicios, ese efecto compuesto de tail latency es enorme.

Serialización JSON (payload 1MB, 10K iteraciones)

// Benchmark: JSON.parse + transformación + JSON.stringify
const payload = generateLargeJSON(1_000_000); // ~1MB
const serialized = JSON.stringify(payload);

console.time('json-roundtrip');
for (let i = 0; i < 10_000; i++) {
 const parsed = JSON.parse(serialized);
 parsed.metadata.processedAt = Date.now();
 parsed.items = parsed.items.filter(i => i.active);
 JSON.stringify(parsed);
}
console.timeEnd('json-roundtrip');
Runtime x86 ARM64 Delta
Node.js 22 34.2s 31.8s -7.0%
Node.js 22 + --max-semi-space-size=128 28.1s 25.4s -9.6%

El parser JSON de V8 se beneficia del bandwidth de memoria mejorado de Graviton4. El flag de semi-space importa más en ARM64 porque el perfil de presión del GC es diferente, el cache L2 más grande de Graviton4 (2MB por core vs 1.25MB en Sapphire Rapids) mantiene más objetos young-gen calientes.

El Gotcha de Node.js: Módulos Nativos

Varias dependencias npm comunes no tienen binarios ARM64 pre-compilados:

  • sharp, tenía binarios ARM64, funcionó perfecto
  • bcrypt, tenía binarios ARM64, funcionó perfecto
  • cpu-features, sin binario ARM64, cayó a WASM, 3x más lento
  • re2, sin binario ARM64, compilar desde source requería python3 y g++ en el container
  • canvas, sin binario ARM64, requería compilación manual con deps del sistema

El fallback de cpu-features es invisible, sin warnings, sin errores, simplemente más lento en silencio. Suele detectarse cuando los benchmarks de validación JWT resultan peores de lo esperado en ARM64, lo que lleva a descubrir que está usando el fallback WASM para la detección de features que hace internamente.

Siempre verificá: npm ls --all | xargs -I {} npm info {} cpu

Go (1.23): Donde ARM64 Realmente Brilla

Go en ARM64 es otra historia. El compilador de Go tiene soporte ARM64 de primera clase desde Go 1.16, y el runtime está fuertemente optimizado para él.

Benchmark de API HTTP (net/http, JSON CRUD, backend PostgreSQL)

Métrica m7i.xlarge (x86) r8g.xlarge (ARM64) Delta
Requests/seg 28,450 34,120 +19.9%
Latencia promedio 3.45ms 2.88ms -16.5%
Latencia p99 11.2ms 8.1ms -27.7%
Allocs/op 42 42 0%
Pausa GC (p99) 0.8ms 0.6ms -25%

Veinte por ciento más de throughput. Eso no es error de redondeo. Y las pausas del GC son más cortas porque el subsistema de memoria de Graviton4 es genuinamente mejor, mayor bandwidth, menor latencia a L2/L3.

El Flag GOARM64: Performance Gratis

Este es el tip que la mayoría se pierde. Go 1.23 introdujo GOARM64 para targeting granular de features ARM64:

# Build default (conservador, corre en cualquier ARM64)
GOOS=linux GOARCH=arm64 go build -o service ./cmd/service

# Build optimizado para Graviton4 (usa features ARMv9 incluyendo SVE2)
GOOS=linux GOARCH=arm64 GOARM64=v9.0 go build -o service ./cmd/service

La diferencia en una operación compute-heavy (un motor de recomendaciones, pesado en math float64):

Build Throughput Latencia p50
ARM64 default 12,340 ops/s 4.1ms
GOARM64=v9.0 14,780 ops/s 3.4ms

Eso es 19.8% más de throughput por una sola variable de entorno. El flag v9.0 habilita instrucciones SVE2 (Scalable Vector Extension 2), que Graviton4 soporta. Para workloads con math de punto flotante, procesamiento de strings u operaciones criptográficas, es performance gratis.

Flags del Compilador con Impacto Medible

Más allá de GOARM64, estos flags mostraron diferencias medibles en ARM64:

# PGO (Profile-Guided Optimization), funciona en ARM64 desde Go 1.21
go build -pgo=default.pgo -o service ./cmd/service

# Resultados con PGO en ARM64:
# Sin PGO: 34,120 req/s
# Con PGO: 37,230 req/s (+9.1%)

PGO en ARM64 es ligeramente más efectivo que en x86 porque el branch predictor de Graviton4 se beneficia más de los datos del profile. Recolectá un CPU profile de producción (runtime/pprof), guardalo como default.pgo en el directorio de tu paquete main, y recompilá. Es genuinamente performance gratis.

Rust (1.82): La Bestia en ARM64

Rust en Graviton4 es donde las cosas se ponen interesantes. El backend ARM64 de LLVM es excepcionalmente maduro, y Rust te da el control para explotarlo.

Benchmark de Servicio de Procesamiento de Imágenes

Un servicio de procesamiento de imágenes que hace redimensionamiento y conversión de formato en tiempo real (WebP, AVIF) provee un benchmark intensivo en SIMD, bandwidth de memoria y cómputo.

// Apuntar a Graviton4 específicamente
// .cargo/config.toml
[target.aarch64-unknown-linux-gnu]
rustflags = ["-C", "target-cpu=neoverse-v2"]
Operación x86 (AVX2) ARM64 (default) ARM64 (neoverse-v2) Delta vs x86
JPEG decode (4K) 12.3ms 14.1ms 11.2ms -8.9%
WebP encode 28.7ms 31.2ms 24.8ms -13.6%
AVIF encode 89.4ms 97.1ms 78.3ms -12.4%
Resize 4K→1080p 8.4ms 9.1ms 7.2ms -14.3%

Observar la diferencia entre ARM64 default y target-cpu=neoverse-v2. Sin el flag de target CPU, ARM64 es en realidad más lento que x86 para workloads SIMD-heavy porque el compilador genera código NEON conservador. Con el flag, usa SVE2 con vectores más amplios y el rendimiento se invierte.

NEON vs SVE2: Diferencias Clave

ARM64 tiene dos sets de instrucciones SIMD:

  • NEON: Vectores fijos de 128 bits, disponible en todo ARM64. Equivalente a SSE4.2.
  • SVE2: Vectores escalables (128-512 bits, Graviton4 usa 128 bits pero con instrucciones adicionales), disponible en ARMv9. Equivalente a AVX2/AVX-512 en capacidad, aunque no en ancho de vector crudo.

Si se está usando intrinsics SIMD directamente en Rust:

// NEON (funciona en todos lados)
#[cfg(target_arch = "aarch64")]
use std::arch::aarch64::*;

unsafe fn sum_neon(data: &[f32]) -> f32 {
 let mut acc = vdupq_n_f32(0.0);
 for chunk in data.chunks_exact(4) {
 let v = vld1q_f32(chunk.as_ptr());
 acc = vaddq_f32(acc, v);
 }
 vaddvq_f32(acc)
}

Para la mayoría del código Rust, no se requieren intrinsics. El compilador auto-vectoriza cuando se configura el target CPU correctamente. Sin embargo, al portar código SIMD de x86, es importante considerar que NEON y AVX tienen semánticas diferentes para algunas operaciones (en particular: no existe equivalente directo de _mm_shuffle_ps, se requiere vqtbl1q_u8 o similar).

Análisis de Costos: Cuatro Meses en Producción

Después de 4 meses en producción, aquí van los números reales:

Costos de Cómputo (14 servicios, us-east-1)

x86 (familia m7i) ARM64 (familia r8g) Ahorro
On-Demand mensual $4,847 $3,878 -20.0%
Reserved 1 año mensual $3,105 $2,484 -20.0%
Spot mensual (promedio) $1,939 $1,242 -35.9%

Los ahorros en Spot son lo destacado. AWS tiene significativamente más capacidad Spot ARM64 (menos gente usándola), así que las tasas de interrupción son más bajas y los precios más estables. En un período de 4 meses, la tasa promedio de interrupción Spot en ARM64 midió 2.1% vs 5.8% en x86.

Ahorro Total en Infraestructura

Combinando la mejora de performance (que permite reducir el tamaño de algunas instancias) con el menor costo por vCPU:

  • Antes (x86, mix On-Demand/Reserved): $3,740/mes
  • Después (ARM64, mix Reserved/Spot): $2,180/mes
  • Ahorro neto: $1,560/mes (-41.7%)

Ese 41.7% no es solo la diferencia de precio ARM64, incluye rightsizing de instancias, ya que los mismos workloads necesitan menos vCPUs en Graviton4.

Checklist de Migración

Una checklist condensada de migración basada en experiencia de producción:

  1. Imágenes base de containers: Cambiá a multi-arch o imágenes arm64v8/ explícitas. FROM node:22-slim ya pullea la arch correcta automáticamente.

  2. CI/CD: Agregar runners ARM64. GitHub Actions ya tiene runs-on: ubuntu-24.04-arm. Si son self-hosted, instancias Graviton3 c7g son excelentes CI runners.

  3. Auditoría de dependencias nativas: Ejecutar tu suite de tests en ARM64 primero. Buscá fallbacks WASM, binarios faltantes, requerimientos de compilación.

  4. Compiler flags: Configurar target CPU para lenguajes compilados. GOARM64=v9.0 para Go, target-cpu=neoverse-v2 para Rust, -march=armv9-a para C/C++.

  5. Monitoreo: Vigilar regresiones de performance en operaciones específicas. Las operaciones crypto comúnmente resultan más lentas inicialmente por falta de configuración del engine OpenSSL para ARM64.

  6. Rollout gradual: Utilizar target groups con pese es en tu load balancer. Empezá con 10% de tráfico a ARM64, validá métricas, incrementá.

Limitaciones y Consideraciones de los Benchmarks

Todo post de benchmarks (incluyendo este) es inherentemente engañoso porque mide workloads específicos en configuraciones específicas. Los resultados van a variar. Algunas áreas se excluyen deliberadamente porque serían engañosas:

  • Workloads memory-bound: El r8g tiene características de RAM diferentes al m7i. La comparación directa es comparar peras con manzanas.
  • Performance pico single-threaded: El turbo boost de x86 puede llegar a 3.6GHz+. Graviton4 corre a 3.0GHz fijos. Para ráfagas de single-thread, x86 gana.
  • Workloads AVX-512: Si tu código usa AVX-512 (inferencia ML, computación científica), x86 tiene una ventaja genuina. El SVE2 de Graviton4 no es un equivalente directo.

El enfoque correcto: ejecutar tu workload de producción real en ambas arquitecturas con tráfico real. Los benchmarks sintéticos te dicen dirección, no magnitud.

¿Vale la Pena?

La reducción de costos del 41% por sí sola justifica las aproximadamente dos semanas de trabajo de migración. Pero el beneficio menos cuantificable es operacional: los cores de frecuencia fija de Graviton4 entregan performance más predecible, lo que significa que el autoscaling es más responsivo y el capacity planning es más simple.

Para workloads basados en AWS, la evaluación de Graviton4 es cada vez más indispensable. El ecosistema ARM64 está lo suficientemente maduro como para que la migración sea aburrida, y en infraestructura, aburrido es exactamente el objetivo.