Ejercicio 4: Parámetros de Inlining del Compilador - Ajuste para el Control del Tamaño del Binario

📖 ¿Quieres aprender más? Lee The IR en Internals for Interns para profundizar en la representación intermedia de Go, incluyendo cómo se toman las decisiones de inlining de funciones.

En este ejercicio, explorarás y modificarás los parámetros de inlining de Go para ver sus efectos dramáticos en el tamaño del binario. Esto te enseñará cómo el compilador de Go decide cuándo hacer inline de funciones y cómo ajustar estos parámetros puede cambiar significativamente tus programas compilados.

Objetivos de Aprendizaje

Al finalizar este ejercicio, serás capaz de:

Introducción: ¿Qué es la IR?

Después del parseo y la verificación de tipos, el compilador convierte el AST en una Representación Intermedia (IR). Mientras el AST refleja lo que escribiste en tu código fuente, la IR es una representación diferente optimizada para el análisis y la transformación por parte del compilador.

La IR representa cada operación en tu código usando ~150 códigos de operación (como OADD, OCALL, OIF). Cada nodo de la IR lleva información de tipos y está organizado por paquete. Esta es la fase donde el compilador toma decisiones de optimización importantes — y una de las más impactantes es el inlining de funciones.

El compilador recorre el árbol IR con un “hairiness visitor” que asigna un coste a cada nodo. Si el coste total de una función se mantiene dentro del presupuesto de inlining (por defecto: 80 nodos), la función es candidata para el inlining. Las llamadas a funciones cuestan 57 nodos, las sentencias simples cuestan 1 nodo. Cuando se hace inline de una función en un punto de llamada, el compilador copia su cuerpo y reemplaza los parámetros por los argumentos.

Puedes observar las decisiones de inlining con go build -gcflags='-m' (o -m=2 para las razones detalladas).

Contexto: Inlining de Funciones en Go

El inlining de funciones es una optimización del compilador donde las llamadas a funciones se reemplazan por el cuerpo real de la función. Esto intercambia tamaño del binario por rendimiento:

Beneficios:

Costes:

Go utiliza un sofisticado sistema de presupuesto para decidir cuándo el inlining es rentable.

Paso 1: Entender el Presupuesto de Inlining de Go

Examinemos los parámetros actuales de inlining:

cd go/src/cmd/compile/internal/inline

Abre inl.go y busca los parámetros clave alrededor de las líneas 49-85:

Parámetros Clave de Inlining

De go/src/cmd/compile/internal/inline/inl.go:49-85:

const (
    inlineMaxBudget       = 80
    inlineExtraAppendCost = 0
    inlineExtraCallCost   = 57              // benchmarked to provide most benefit
    inlineParamCallCost   = 17              // calling a parameter costs less
    inlineExtraPanicCost  = 1               // do not penalize inlining panics
    inlineExtraThrowCost  = inlineMaxBudget // inlining runtime.throw does not help

    inlineBigFunctionNodes      = 5000                 // Functions with this many nodes are "big"
    inlineBigFunctionMaxCost    = 20                   // Max cost when inlining into a "big" function
    inlineClosureCalledOnceCost = 10 * inlineMaxBudget // if a closure is called once, inline it
)

var (
    // ...
    // Budget increased due to hotness (PGO).
    inlineHotMaxBudget int32 = 2000
)

Nota: inlineHotMaxBudget es una var, no una const, porque se usa con PGO (Profile Guided Optimization) y puede modificarse en tiempo de ejecución.

Cómo Funciona el Sistema de Presupuesto

Cada sentencia/expresión de Go tiene un coste:

El compilador suma los costes y los compara con el presupuesto.

Paso 2: Usar el Binario del Compilador de Go para Comparar Tamaños

En lugar de crear programas de juguete, usemos el propio binario del compilador de Go como sujeto de prueba. El compilador de Go (bin/go) es perfecto para demostrar los efectos del inlining porque:

Probar Diferentes Configuraciones de Inlining en el Binario de Go

Recompilemos toda la cadena de herramientas de Go con diferentes configuraciones de inlining y comparemos los tamaños del binario bin/go:

cd go/src

Compilación Base - Configuración por Defecto

Primero, compilemos con la configuración de inlining por defecto y hagamos una copia de seguridad del binario:

# Build with default settings
./make.bash

# Copy the default Go binary for comparison
cp ../bin/go ../bin/go-default

# Check the size
ls -lh ../bin/go-default
wc -c ../bin/go-default

Verificar el Impacto Actual del Inlining en la Compilación del Compilador de Go

Podemos examinar cómo el inlining afecta al propio compilador de Go durante la compilación:

# See inlining decisions when compiling the Go compiler
# This shows how inlining parameters affect the compiler's own build process
cd cmd/compile
../../bin/go build -gcflags="-m" . 2>&1 | grep "can inline" | wc -l
echo "Functions that can be inlined during Go compiler build"

Paso 3: Modificar los Parámetros de Inlining

¡Ahora modifiquemos los parámetros de inlining para ver sus efectos!

Experimento 1: Inlining Agresivo

Edita go/src/cmd/compile/internal/inline/inl.go alrededor de la línea 50:

const (
    inlineMaxBudget       = 95    // Increased from 80
    inlineExtraCallCost   = 40    // Decreased from 57
    inlineBigFunctionMaxCost = 30 // Increased from 20
)

⚠️ Nota: ¡Ten cuidado de no aumentar estos valores demasiado! En Go 1.26.1, el runtime tiene restricciones estrictas de write barrier, y aumentar el presupuesto de inlining más allá de ~95 hace que el compilador haga inline de funciones en contextos donde las write barriers están prohibidas, rompiendo la compilación. Esto en sí mismo es una gran lección sobre el delicado equilibrio de los parámetros del compilador.

Recompila el compilador:

cd go/src
./make.bash

Prueba el inlining agresivo en el binario de Go:

# Copy the aggressively-inlined Go binary
cp ../bin/go ../bin/go-aggressive

# Compare sizes
echo "Default size: $(wc -c < ../bin/go-default)"
echo "Aggressive size: $(wc -c < ../bin/go-aggressive)"

# Calculate size difference
default_size=$(wc -c < ../bin/go-default)
aggressive_size=$(wc -c < ../bin/go-aggressive)
echo "Size difference: $(($aggressive_size - $default_size)) bytes"
echo "Percentage increase: $(echo "scale=2; ($aggressive_size - $default_size) * 100 / $default_size" | bc)%"

Experimento 2: Inlining Conservador

Ahora prueba con configuraciones conservadoras. Edita los parámetros:

const (
    inlineMaxBudget       = 40    // Decreased from 80
    inlineExtraCallCost   = 100   // Increased from 57
    inlineBigFunctionMaxCost = 5  // Decreased from 20
)

Recompila y prueba:

cd go/src
./make.bash

# Copy the conservatively-inlined Go binary
cp ../bin/go ../bin/go-conservative

# Compare all three Go binaries
echo "Conservative size: $(wc -c < ../bin/go-conservative)"
echo "Default size: $(wc -c < ../bin/go-default)"
echo "Aggressive size: $(wc -c < ../bin/go-aggressive)"

Paso 4: Análisis Exhaustivo del Tamaño del Binario

Probemos configuraciones extremas de inlining para ver efectos dramáticos en el binario del compilador de Go:

Experimento 3: Sin Inlining

Para comparar, desactivemos el inlining por completo:

const (
    inlineMaxBudget       = 0     // No inlining budget
    inlineExtraCallCost   = 1000  // Prohibitive call cost
    inlineBigFunctionMaxCost = 0  // No big function inlining
)
cd go/src
./make.bash

# Copy the no-inlining Go binary
cp ../bin/go ../bin/go-no-inline

Experimento 4: Inlining Extremo - Demostración del Punto de Ruptura

Probemos configuraciones extremadamente agresivas para ver qué pasa cuando llevamos el inlining demasiado lejos:

const (
    inlineMaxBudget       = 500   // Very high budget
    inlineExtraCallCost   = 5     // Very low call cost
    inlineBigFunctionMaxCost = 200 // Very high big function budget
)
cd go/src
./make.bash

⚠️ Resultado esperado: ¡Esto fallará al compilar! Verás errores de “write barrier prohibited by caller”. Esto ocurre porque el compilador hace inline de funciones del runtime en contextos donde las write barriers no están permitidas, creando cadenas de llamadas ilegales.

Si falla (que es lo esperado), aprenderás que: - El inlining extremo causa violaciones de write barrier en el runtime - El runtime de Go tiene anotaciones //go:nowritebarrierrec que prohíben write barriers en ciertas cadenas de llamadas - Cuando el inlining expone estas cadenas, el compilador rechaza correctamente la compilación - Los parámetros por defecto están cuidadosamente equilibrados por una buena razón

Paso 5: Analizar los Resultados

Compara los tamaños del binario del compilador de Go:

cd go

echo "=== GO COMPILER BINARY SIZE COMPARISON ==="
echo "No Inlining:  $(wc -c < bin/go-no-inline) bytes"
echo "Conservative: $(wc -c < bin/go-conservative) bytes"
echo "Default:      $(wc -c < bin/go-default) bytes"
echo "Aggressive:   $(wc -c < bin/go-aggressive) bytes"

echo ""
echo "=== SIZE DIFFERENCES ==="
no_inline_size=$(wc -c < bin/go-no-inline)
conservative_size=$(wc -c < bin/go-conservative)
default_size=$(wc -c < bin/go-default)
aggressive_size=$(wc -c < bin/go-aggressive)

echo "No-inline vs Default: $(($default_size - $no_inline_size)) bytes difference"
echo "Default vs Aggressive: $(($aggressive_size - $default_size)) bytes difference"
echo "Full Range (No-inline to Aggressive): $(($aggressive_size - $no_inline_size)) bytes difference"

# Calculate percentages
echo ""
echo "=== PERCENTAGE DIFFERENCES ==="
echo "Aggressive vs Default: $(echo "scale=2; ($aggressive_size - $default_size) * 100 / $default_size" | bc)%"
echo "Default vs No-inline: $(echo "scale=2; ($default_size - $no_inline_size) * 100 / $no_inline_size" | bc)%"

Qué Hemos Modificado

Funciones de los Parámetros Clave

Parámetro Propósito Impacto
inlineMaxBudget Coste máximo para cualquier función inlined Mayor = más inlining
inlineExtraCallCost Penalización por llamadas a funciones dentro de funciones inlined Menor = más agresivo
inlineBigFunctionMaxCost Coste máximo al hacer inline en funciones grandes Mayor = más inlining en funciones grandes
inlineBigFunctionNodes Umbral para la detección de funciones “grandes” Menor = más funciones consideradas “grandes”

Resultados Típicos que Deberías Observar

Con el binario del compilador de Go, deberías observar diferencias de tamaño notables:

Ideas clave:

Los tamaños exactos dependen de tu sistema, pero deberías ver diferencias dramáticas similares.

Lo que Aprendimos

Ideas de Extensión

Prueba estos experimentos adicionales:

  1. Crear un script para automatizar las pruebas con diferentes combinaciones de parámetros
  2. Probar con programas Go del mundo real (¡como compilar el propio Go!)
  3. Medir las diferencias en tiempo de compilación con varias configuraciones
  4. Experimentar con los parámetros de PGO (Profile-Guided Optimization)
  5. Analizar las diferencias en la salida de ensamblador entre llamadas con y sin inline

Siguientes Pasos

Has aprendido cómo ajustar el comportamiento de inlining de Go y has visto su impacto real en el tamaño del binario y el rendimiento. En los próximos ejercicios, exploraremos la modificación de la herramienta gofmt.

Limpieza

Para restaurar los parámetros originales de inlining y limpiar los binarios de prueba:

cd go/src/cmd/compile/internal/inline
git checkout inl.go
cd ../../../../

# Rebuild with original parameters
cd src
./make.bash

# Clean up test binaries
rm -f ../bin/go-default ../bin/go-aggressive ../bin/go-conservative ../bin/go-no-inline

Conclusiones Clave

  1. El Inlining es un Compromiso: Más inlining = binarios más grandes pero potencialmente ejecución más rápida
  2. Sistema de Presupuesto: Go utiliza un sofisticado análisis de costes para tomar decisiones de inlining
  3. Impacto de los Parámetros: Pequeños cambios en los parámetros pueden tener efectos significativos en la salida
  4. Herramientas de Depuración: Go proporciona excelentes herramientas para entender las decisiones del compilador
  5. Relevancia en el Mundo Real: Estos parámetros afectan a cada programa Go que compilas

El equipo del compilador de Go ha ajustado cuidadosamente estos valores por defecto mediante pruebas de rendimiento exhaustivas, pero ahora entiendes cómo ajustarlos para tus necesidades específicas.


Continúa con el Ejercicio 5 o vuelve al taller principal