Why does my linked list node occupies 16 bytes yet malloc reports 24 bytes?
Summary
A developer implemented a linked list node (struct Node) in C using manual memory allocation. While sizeof(struct Node) returned 16 bytes, malloc_usable_size() reported 24 bytes for an allocated node. Additionally, observed memory addresses between consecutive nodes differed by 32 bytes. This raised questions about actual memory consumption and heap allocation behavior in glibc on x86_64 systems.
Root Cause
malloc_usable_size()reports usable space including allocator overhead, not just requested size- glibc’s memory allocator adds 16 bytes of metadata per chunk (8-byte header prefix + 8-byte alignment constraints)
- Small allocations in glibc require a minimum of 24 bytes to accommodate metadata and alignment
- Observed 32-byte address differences reveal actual heap chunk size after metadata/tracking overhead
Why This Happens in Real Systems
- Memory allocators universally add metadata for tracking allocations
- 64-bit systems mandate stricter alignment constraints (16-byte boundaries)
- Heap fragmentation prevention forces rounding up to predefined size classes
- Minimum allocation thresholds exist to optimize allocator performance
- Real-world allocators prioritize stability/prevention of fragmentation over exact sizing
Real-World Impact
- Memory calculations using
sizeofunderestimate actual heap consumption by up to 50% - Linked lists of small objects suffer significant memory overhead
- Performance-critical systems face unexpected memory bloat
- Memory fragmentation increases faster than naive sizing predicts
- Resource-constrained environments (embedded systems) exceed budgets
Example Code
#include
#include
#include
struct Node {
int data; // 4 bytes
struct Node *next; // 8 bytes (64-bit pointer)
}; // Total: 16 bytes (after padding to 8-byte alignment)
int main(void) {
struct Node *n = malloc(sizeof(struct Node)); // Requests 16 bytes
printf("sizeof(struct Node): %ld\n", sizeof(struct Node)); // Output: 16
printf("malloc_usable_size: %ld\n", malloc_usable_size(n)); // Output: 24
free(n);
return 0;
}
How Senior Engineers Fix It
- Validate with
malloc_usable_size: Always verify actual allocation sizes during debugging - Pool allocation: Pre-allocate node arrays to distribute metadata overhead
- Size-conscious structuring: Design structs to minimize internal fragmentation
- Custom allocators: Implement slab allocators for high-node-count scenarios
- Override malloc: Use sized-based allocators like jemalloc/tcmalloc when appropriate
- Memory profiling: Integrate tools like Valgrind to catch unbalanced overhead
- Document overhead: Explicitly account for allocator metadata in capacity planning
Why Juniors Miss It
- Language specification gap:
sizeofmeasures struct layout only, not runtime allocation - Undocumented behaviors: Allocator internals aren’t covered in introductory materials
- Hidden costs: Memory overhead isn’t visible in standard debugging workflows
- Misinterpreted pointers: Confusion between struct size and allocated block boundaries
- Tool unfamiliarity: Underutilization of diagnostic functions like `malloc_usable_size