Why does my linked list node occupies 16 bytes and why does malloc_usable_size() returns 24 bytes?

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 sizeof underestimate 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: sizeof measures 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