Fundamentals 23 min read

What Hidden Costs Does C++ new Add to Heap Allocation?

The article reveals that using C++ new or malloc is far from cost‑free, detailing how Linux virtual address space, system‑call overhead, memory‑header metadata, fragmentation, and page‑fault latency all contribute to hidden performance penalties, and it offers concrete techniques such as stack allocation, memory pools, restrained PImpl usage, and startup pre‑allocation to mitigate these costs.

Deepin Linux
Deepin Linux
Deepin Linux
What Hidden Costs Does C++ new Add to Heap Allocation?

Understanding Linux Process Virtual Address Space

Each Linux process has its own virtual address space, a private view that maps to physical memory via page tables. The space is divided into code, data (initialized and BSS), heap, stack, and other regions such as read‑only data and shared memory.

The virtual address is translated to a physical address by the kernel using a page table, which records the mapping of 4 KB pages. When a process accesses a virtual address, the CPU looks up the corresponding page‑table entry to obtain the physical page number.

Case Study: Fork‑Based Heap Isolation

#include <stdio.h>
#include <unistd.h>
int global = 10;
int main(){
    pid_t pid = fork();
    if(pid == 0){
        global = 20;
        printf("Child: global = %d, &global = %p
", global, &global);
    } else if(pid > 0){
        sleep(1);
        printf("Parent: global = %d, &global = %p
", global, &global);
    } else {
        perror("fork error");
        return 1;
    }
    return 0;
}

Running this program shows that the parent and child share the same virtual address for global, but after the child writes, the values differ because the kernel employs copy‑on‑write: it allocates a new physical page for the child and updates the child's page table.

new Heap Allocation Overhead Analysis

In C++, new allocates memory from the heap and returns a pointer. The allocation process consists of three steps:

Memory Lookup : The allocator scans its internal data structures (e.g., a free‑list) to find a block large enough for the request.

Memory Allocation : The chosen block is marked as used; if it is larger than needed, it may be split, returning the requested portion and keeping the remainder on the free list.

Optional Initialization : For fundamental types, the memory is uninitialized unless a constructor or new T(value) is used; for class types, the constructor runs after the block is obtained.

The hidden costs of heap allocation fall into four categories:

System‑call and context‑switch overhead : Expanding the heap or requesting new pages forces a transition from user mode to kernel mode, involving register saving, permission checks, page‑table lookups, and other kernel work. Frequent small allocations can cause high CPU usage and thread‑scheduling stalls.

Memory‑header (chunk) overhead : Each allocated chunk carries metadata (size, status, links, flags). Even a 4‑byte int allocation consumes many more bytes because of this header, reducing memory utilization and increasing traversal cost for the allocator.

Fragmentation overhead : Repeated allocate‑free cycles produce non‑contiguous free blocks. When a large block is later needed, the allocator may spend extra time scanning many small fragments or may fail despite sufficient total free memory, and cache locality suffers.

Page‑fault (lazy‑allocation) overhead : Linux often delays physical page allocation until the first read/write of the newly allocated virtual region. The first access triggers a page‑fault interrupt, causing the kernel to allocate a physical page, set up the mapping, and update permissions, which appears as a sudden latency spike.

Two primary factors influence the magnitude of these costs:

Allocation size : Small allocations incur a higher proportion of metadata overhead (e.g., an 8‑byte header is 80 % of a 10‑byte request but only 0.8 % of a 1000‑byte request).

Heap fragmentation level : Heavy fragmentation forces the allocator to search more free blocks, increasing lookup time and possibly triggering additional system calls for larger requests.

Four Ways to Avoid High new Overhead

(1) Prefer Stack Allocation for Small Objects

For short‑lived, small objects (structs, utility classes, temporary buffers), declare them on the stack. Stack allocation merely moves the stack pointer in user mode, incurring virtually no overhead and eliminating heap‑related costs.

void bad_func(){
    int* tmp = new int(100); // incurs system call, header, possible page fault
    // ...
    delete tmp;
}

void good_func(){
    int tmp = 100; // pure stack allocation, no kernel interaction
    // ...
}

(2) Use Memory Pools for High‑Frequency Small Objects

Allocate a large contiguous block once (single system call) and sub‑allocate from it in user space. This removes repeated kernel transitions, chunk‑header creation, and fragmentation.

char* pool = new char[1024 * 1024];
size_t offset = 0;
void* pool_alloc(size_t size){
    void* ret = pool + offset;
    offset += size;
    return ret;
}
int main(){
    int* p1 = (int*)pool_alloc(4);
    int* p2 = (int*)pool_alloc(4);
    return 0;
}

(3) Avoid Overusing PImpl for Tiny Classes

PImpl hides implementation behind a heap‑allocated pointer, adding a heap allocation and its associated overhead. Use it only for large, stable components; for tiny, frequently created classes, keep data members inline.

// Bad: each instance incurs a heap allocation
class BadDemo{
public:
    BadDemo(): impl(new Impl) {}
private:
    struct Impl{ int val; }* impl;
};

// Good: data stored directly, no heap cost
class GoodDemo{
private:
    int val;
};

(4) Pre‑allocate at Startup to Eliminate Runtime Overheads

Perform a one‑time large allocation during program initialization, bind physical pages, and reuse the memory throughout the program’s lifetime. This avoids runtime page‑faults, system calls, and fragmentation.

char* g_buf = nullptr;
void init(){
    g_buf = new char[1024 * 1024];
    memset(g_buf, 0, 1024 * 1024);
}
void business_run(){
    memcpy(g_buf, "data", 4); // no further allocations
}
int main(){
    init();
    while(1){ business_run(); }
}

From the Linux virtual‑address‑space perspective, new is never free; it incurs system‑call, context‑switch, metadata, fragmentation, and lazy‑allocation costs. Most C++ performance stalls, memory leaks, and resource waste stem from indiscriminate heap usage. High‑performance development therefore means minimizing new, using it precisely, and avoiding its hidden costs.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

performanceC++linuxheapmemory allocationnew operator
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.