Alineación y padding en structs de Go

Cuando defines un struct, no puedes asumir que el tamaño total será la suma exacta de los tamaños de sus campos. El compilador de Go aplica una estrategia de alineación para que la CPU acceda a los datos de forma eficiente. En la mayoría de las arquitecturas de 64 bits, un tipo como int64 requiere estar posicionado en una dirección de memoria que sea múltiplo de 8. Si intentas colocar un int64 inmediatamente después de un bool, el compilador insertará bytes de padding (espacios en blanco invisibles) para asegurar que ese int64 esté correctamente alineado.

Esta decisión de diseño busca evitar que la CPU tenga que realizar múltiples ciclos de lectura para obtener un solo valor que cruza una frontera de palabra de memoria. Si trabajas con pocos objetos, esto es irrelevante. Sin embargo, cuando manejas slices con millones de elementos o estructuras que viven en el hot path de tus algoritmos, el padding innecesario aumenta el consumo de memoria y, lo que es peor, reduce la densidad de la caché L1/L2, provocando más cache misses y degradando el rendimiento global.

Para evitar este desperdicio, la regla de oro es ordenar los campos de mayor a menor tamaño. Si no lo haces, el compilador simplemente rellenará los huecos para que el programa sea seguro y rápido, pero tu estructura será más pesada de lo que debería.

package main

import (
	"fmt"
	"unsafe"
)

// BadStruct es una estructura mal diseñada desde el punto de vista de la memoria.
// Los campos están intercalados, lo que obliga al compilador a insertar padding.
type BadStruct struct {
	A bool   // 1 byte
	// [7 bytes de padding aquí para que B esté en un múltiplo de 8]
	B int64  // 8 bytes
	C bool   // 1 byte
	// [6 bytes de padding al final para alinear el tamaño total al múltiplo de 8]
}

// GoodStruct es la versión optimizada.
// Agrupamos los campos por tamaño para minimizar el padding.
type GoodStruct struct {
	B int64  // 8 bytes
	A bool   // 1 byte
	C bool   // 1 byte
	// [6 bytes de padding al final para completar el alineamiento de 8]
}

func main() {
	// Instanciamos ambas estructuras
	bad := BadStruct{A: true, B: 42, C: false}
	good := GoodStruct{A: true, B: 42, C: false}

	// Comprobamos el tamaño total de cada struct
	fmt.Printf("Tamaño de BadStruct:  %d bytes\n", unsafe.Sizeof(bad))
	fmt.Printf("Tamaño de GoodStruct: %d bytes\n\n", unsafe.Sizeof(good))

	// Inspeccionamos los desplazamientos (offsets) para ver dónde está el padding
	fmt.Printf("Offset de BadStruct.B:  %d\n", unsafe.Offsetof(bad.B))
	fmt.Printf("Offset de GoodStruct.B: %d\n", unsafe.Offsetof(good.B))
}

Fíjate en la salida de unsafe.Sizeof. Aunque la suma de los tipos en BadStruct es de apenas 10 bytes (1+8+1), el compilador reportará 24 bytes. Esto sucede porque B no puede empezar en el byte 1; necesita empezar en el byte 8. Para lograrlo, “paga” con 7 bytes de padding después de A. Además, el tamaño total del struct debe ser múltiplo de su alineación máxima (8), por lo que se añaden bytes al final.

En cambio, en GoodStruct, al colocar B al principio, el offset de B es 0. Los campos A y C se colocan uno tras otro y, al final, solo se necesitan 6 bytes de padding para completar el bloque de 8. El tamaño total cae a 16 bytes. En un array de un millón de elementos, esa diferencia de 8 bytes por struct se traduce en 8 MB de memoria desperdiciada y una carga de caché mucho más ineficiente.

El uso de unsafe.Offsetof en el ejemplo te permite ver exactamente en qué posición de memoria comienza cada campo. Si el offset de un int64 no es un múltiplo de 8, tienes un problema de alineación, aunque Go garantiza que esto no pasará en un programa bien formado; el padding es la forma que tiene el compilador de asegurar esa alineación por ti.

El error frecuente

Un error común es intentar optimizar el tamaño de un struct pensando que “ahorrar bytes” siempre es bueno. Si tienes un struct que se usa de forma aislada, el orden de los campos no importa. El verdadero problema ocurre cuando estos structs son elementos de un slice masivo. En esos casos, un mal orden puede duplicar el uso de la memoria de tu aplicación y aumentar drásticamente la presión sobre el Garbage Collector debido al mayor tamaño de los objetos en el heap.

Si sospechas que tus estructuras están desperdiciando memoria, utiliza la herramienta fieldalignment que forma parte de los paquetes de análisis de Go (golang.org/x/tools/go/analysis/passes/fieldalignment). Esta herramienta detecta automáticamente structs con padding innecesario y te indica cómo reordenarlos para mejorar la densidad de datos en memoria.

63

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio