Las listas enlazadas tradicionales añaden un nodo por fuera del objeto que queremos encadenar. Eso implica punteros extra, saltos de caché y, sobre todo, memoria desperdiciada. La solución que usan sistemas como Nginx o TCMalloc es mucho más agresiva: incrustar el puntero next dentro del propio objeto. De ahí el nombre intrusive: la estructura "invade" la memoria del dato.
¿Cómo se ve en código?
// Puntero next almacenado en los primeros 8 bytes del bloque
static inline void*& siguiente(void* bloque) noexcept {
return *reinterpret_cast<void**>(bloque);
}
void* a = malloc(1024);
void* b = malloc(1024);
siguiente(a) = b; // a → b
siguiente(b) = nullptr;
Con esta única línea hemos convertido cualquier región de memoria en un nodo de lista sin coste adicional.
Comparativa rápida
| Enfoque | Bytes extra por nodo | Asignaciones | Localidad |
|---|---|---|---|
| Lista clásica | 16 (puntero data + puntero next) | 2 (dato + nodo) | Baja |
| Intrusiva | 0 | 1 (solo el dato) | Alta |
Una FreeList mínima
class FreeList {
public:
void liberar(void* p) noexcept {
siguiente(p) = cabeza_;
cabeza_ = p;
++cnt_;
}
void* adquirir() noexcept {
if (!cabeza_) return nullptr;
void* p = cabeza_;
cabeza_ = siguiente(p);
--cnt_;
return p;
}
// Inserción masiva: O(1)
void liberar_rango(void* primero, void* ultimo, std::size_t n) noexcept {
siguiente(ultimo) = cabeza_;
cabeza_ = primero;
cnt_ += n;
}
private:
void* cabeza_ = nullptr;
std::size_t cnt_ = 0;
};
Por qué escala tan bien
- Localidad temporal y espacial: el puntero
nexty el objeto residen en la misma línea de caché. - Menor presión sobre el allocator: solo se pide memoria para el objeto real.
- Operaciones vectorizables: rangos completos se mueven con dos escrituras (
siguiente(ultimo)ycabeza_).
Ejemplo real: ThreadCache simplificado
void* ThreadCache::asignar(std::size_t bytes) {
const std::size_t idx = indice(bytes);
if (!listas_[idx].vacia())
return listas_[idx].adquirir();
// CentralCache nos entrega un lote completo
return obtener_desde_central(idx);
}
void ThreadCache::devolver(void* p, std::size_t bytes) {
const std::size_t idx = indice(bytes);
listas_[idx].liberar(p);
if (listas_[idx].tamano() >= listas_[idx].maximo())
regresar_a_central(idx); // liberar_rango evita locks
}
Consejos de implementación
- Asegura que
sizeof(T) ≥ sizeof(void*)y alineación correcta. - Para depurar, reemplaza
siguientepor un campo nombrado durante las compilacionesDebug. - Si el objeto se destruye, elimínalo de la lista antes; de lo contrario quedará un puntero colgando.
- En entornos multihilo, protege la cabeza con un
std::atomic<void*>o haz pop local por hilo.
Usos más allá del memory pool
// Pool de balas en un motor de juegos
class BulletPool {
FreeList libres;
public:
Bullet* disparar() { return static_cast<Bullet*>(libres.adquirir()); }
void reciclar(Bullet* b) { libres.liberar(b); }
};
// Cola de eventos sin nodos extra
class EventQueue {
FreeList pendientes;
public:
void encolar(Event* e) { pendientes.liberar(e); }
Event* desencolar() { return static_cast<Event*>(pendientes.adquirir()); }
};
Con este patrón, cualquier estructura que necesites gestionar de forma ligera (páginas, sockets, tareas) puede beneficiarse de listas intrusivas: cero overhead, máxima velocidad y un diseño que escala hasta el núcleo del sistema operativo.