惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
The Register - Security
The Register - Security
MongoDB | Blog
MongoDB | Blog
U
Unit 42
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
L
LINUX DO - 最新话题
博客园_首页
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
P
Privacy & Cybersecurity Law Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
C
Cisco Blogs
博客园 - 【当耐特】
阮一峰的网络日志
阮一峰的网络日志
I
Intezer
罗磊的独立博客
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
A
About on SuperTechFans
G
GRAHAM CLULEY
Y
Y Combinator Blog
Microsoft Security Blog
Microsoft Security Blog
GbyAI
GbyAI
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
D
DataBreaches.Net
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
I
InfoQ
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 叶小钗
Project Zero
Project Zero

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Avoiding The GC in D: Stack, Buffers & Arenas
Kapendev · 2026-05-09 · via DEV Community

It's common to see new D users talking about @nogc and -betterC when discussing Manual Memory Management (MMM).
In a way, it makes sense.
Both are closely related to it based on their names.
One of them is literally called "no GC."

This might sound strange given their names, but neither @nogc nor -betterC is actually MMM.
They don't tell you how memory is allocated or when allocation happens.
A @nogc function could allocate or it could not.
What they do tell you is that one type of allocator, the GC, is not being used.
That only rules out one allocator.

The TL;DR is that both of those features are compiler-enforced GC restrictions with:

  • @nogc: also being part of the type system.
  • -betterC: also removing the D runtime.

In this post, I will skip over -betterC since removing the D runtime is a topic of its own.
I'll go over how to do manual memory management in D and how to avoid the GC without fighting the language.

Stack Memory

Let's start with the stack.
It's the simplest option to use and is always available.
To use it, you define a static type and create an instance of it on the stack:

void main() {
    alias StringData = char[64][4]; // The type.
    StringData stringData = void;   // The instance.
}

Enter fullscreen mode Exit fullscreen mode

In the code above, the program can store 4 strings containing up to 64 characters each.
The = void part tells the compiler to not default initialize the string data because it will be initialized later as needed.

For example:

import std.stdio;

void main() {
    alias StringData = char[64][4];
    StringData stringData = void;

    auto text = stringData[0][0 .. 3]; // Create a string.
    text[] = "Hi!";                    // Initialize the string.
    writeln(text);                     // Use the string.
}

Enter fullscreen mode Exit fullscreen mode

The variable text points to the 3 first bytes of stringData and they are used to print Hi! with writeln.
A similar pattern in game development is creating temporary strings for things like entities or levels.
With the trick above, we can generate level names without dynamically allocating:

import std.stdio;

void main() {
    alias StringData = char[64][4];
    StringData stringData = void;

    auto level = 68;
    auto name = stringData[0][0 .. 8];
    name[0 .. 6] = "level_";
    name[6] = cast(char) ('0' + (level / 10) % 10);
    name[7] = cast(char) ('0' + level % 10);

    writeln(name); // Output: level_68
}

Enter fullscreen mode Exit fullscreen mode

That's nice.
There is no need to free any memory here because the stack will handle it automatically.
The natural next step is to wrap the string generation part of the code into a function so the main function is less noisy:

import std.stdio;

void main() {
    auto level = 68;
    auto name = level.toLevelName();
    writeln(name);
}

const(char)[] toLevelName(int level) {
    alias StringData = char[64][4];
    StringData stringData = void;

    auto name = stringData[0][0 .. 8];
    name[0 .. 6] = "level_";
    name[6] = cast(char) ('0' + (level / 10) % 10);
    name[7] = cast(char) ('0' + level % 10);
    return name;
}

Enter fullscreen mode Exit fullscreen mode

You might notice, however, that the output is now not correct.
This happens because the returned slice points to memory owned by the callee's stack frame, which becomes invalid once the function returns.
So, how do we fix that if we want to be able to return some temporary data from a function?

Static Buffers

The solution is static buffers.
They are valid for the entirety of the program's lifetime, meaning we can pass them around without any issues.
It's also important to note that static variables in D are thread-local by default, so each thread gets its own buffer:

import std.stdio;

void main() {
    auto level = 68;
    auto name = level.toLevelName();
    writeln(name);
}

const(char)[] toLevelName(int level) {
    alias StringData = char[64][4];
    static StringData stringData = void; // <-- The fix.

    auto name = stringData[0][0 .. 8];
    name[0 .. 6] = "level_";
    name[6] = cast(char) ('0' + (level / 10) % 10);
    name[7] = cast(char) ('0' + level % 10);
    return name;
}

Enter fullscreen mode Exit fullscreen mode

There is still one issue though: only one of the 4 static strings is being used.
This means that every call to toLevelName will always return the same data, potentially invalidating the memory of any variable that holds a result of that function.

For example:

import std.stdio;

void main() {
    auto name1 = 1.toLevelName();
    auto name2 = 2.toLevelName();
    writeln(name1); // Output: level_02
    writeln(name2); // Output: level_02
}

const(char)[] toLevelName(int level) {
    alias StringData = char[64][4];
    static StringData stringData = void;

    auto name = stringData[0][0 .. 8];
    name[0 .. 6] = "level_";
    name[6] = cast(char) ('0' + (level / 10) % 10);
    name[7] = cast(char) ('0' + level % 10);
    return name;
}

Enter fullscreen mode Exit fullscreen mode

Both name1 and name2 should have different values, yet they are the same because of the mentioned issue.
To avoid this, a new static variable needs to be created that changes the current string used by the toLevelName function:

import std.stdio;

void main() {
    auto name1 = 1.toLevelName();
    auto name2 = 2.toLevelName();
    writeln(name1); // Output: level_01
    writeln(name2); // Output: level_02
}

const(char)[] toLevelName(int level) {
    alias StringData = char[64][4];
    static StringData stringData = void;
    static ubyte stringDataIndex = 0; // <-- Fix 1.

    stringDataIndex = (stringDataIndex + 1) % stringData.length; // <-- Fix 2.
    auto name = stringData[stringDataIndex][0 .. 8]; // <-- Fix 3.
    name[0 .. 6] = "level_";
    name[6] = cast(char) ('0' + (level / 10) % 10);
    name[7] = cast(char) ('0' + level % 10);
    return name;
}

Enter fullscreen mode Exit fullscreen mode

This works because stringDataIndex cycles through the 4 available strings in order, wrapping back to the beginning once it reaches the end.
This is called a circular buffer, and it is a common pattern for managing a fixed pool of temporary data without dynamic allocation.

Static buffers work well for short-lived data with a known maximum size, but they force the function to manage its own memory.
What if you don't want that?

Passing Memory

One solution is to pass the memory to it instead:

import std.stdio;

void main() {
    char[256] buffer = void;
    auto name1 = 1.toLevelName(buffer[0 .. 16]);
    auto name2 = 2.toLevelName(buffer[16 .. 32]);
    writeln(name1); // Output: level_01
    writeln(name2); // Output: level_02
}

const(char)[] toLevelName(int level, char[] data) {
    auto name = data[0 .. 8];
    name[0 .. 6] = "level_";
    name[6] = cast(char) ('0' + (level / 10) % 10);
    name[7] = cast(char) ('0' + level % 10);
    return name;
}

Enter fullscreen mode Exit fullscreen mode

The buffer here is a stack allocation in main, but it could come from anywhere.
It could be memory from malloc or even the GC.

This example works, but manually tracking which slice of the buffer is free like the code above quickly becomes tedious.
That's the job of an allocator.

Allocators

At this point we are manually tracking regions in a buffer.
Allocators exist to automate exactly that.
For this code, it makes sense to make an arena/bump allocator.
An arena allocator works by keeping a pointer into a buffer and bumping it forward with each allocation.
When the buffer is full, it returns null.

The arena will look like this:

struct Arena {
    char[] buffer;
    size_t offset;

    char[] makeChars(size_t size) {
        if (offset + size > buffer.length) return null;
        auto result = buffer[offset .. offset + size];
        offset += size;
        return result;
    }
}

Enter fullscreen mode Exit fullscreen mode

Then update toLevelName to use it:

const(char)[] toLevelName(int level, ref Arena arena) {
    auto name = arena.makeChars(8);
    if (name.length == 0) return null;
    name[0 .. 6] = "level_";
    name[6] = cast(char) ('0' + (level / 10) % 10);
    name[7] = cast(char) ('0' + level % 10);
    return name;
}

Enter fullscreen mode Exit fullscreen mode

And finally update main:

void main() {
    char[256] buffer = void;
    auto arena = Arena(buffer);
    auto name1 = 1.toLevelName(arena);
    auto name2 = 2.toLevelName(arena);
    writeln(name1); // Output: level_01
    writeln(name2); // Output: level_02
}

Enter fullscreen mode Exit fullscreen mode

The arena owns the buffer and handles the slicing.
To free all the memory at once, just reset the offset back to zero.

Abstracting Allocators

Like every solution before, passing allocators around has also an issue: they color functions, meaning a specific allocator must be passed to every function that needs it.
A way to somewhat avoid this is by abstracting allocators behind one or more function pointers.
That mechanism is referred to as an allocator API.
It is slower than using them directly, but avoids specialization.
Whether that tradeoff makes sense depends on the situation.

If you need one, below is the allocator API of Joka that can be easily copy-pasted into a project:

struct MemoryContext {
    void* allocatorState;
    AllocatorReallocFunc reallocFunc;

    void* malloc(size_t alignment, size_t size, const(char)[] file, size_t line) {
        return reallocFunc(allocatorState, alignment, null, 0, size, file, line);
    }

    void* realloc(size_t alignment, void* oldPtr, size_t oldSize, size_t newSize, const(char)[] file, size_t line) {
        return reallocFunc(allocatorState, alignment, oldPtr, oldSize, newSize, file, line);
    }

    void free(size_t alignment, void* oldPtr, size_t oldSize, const(char)[] file, size_t line) {
        reallocFunc(allocatorState, alignment, oldPtr, oldSize, 0, file, line);
    }
}

alias AllocatorReallocFunc = void* function(void* allocatorState, size_t alignment, void* oldPtr, size_t oldSize, size_t newSize, const(char)[] file, size_t line);

Enter fullscreen mode Exit fullscreen mode

What About The GC?

Now that we covered the basics, let's focus on a common question: how do I know that the GC is not being used without @nogc?
There are some things to be aware of.

The first is array literals.
They allocate when assigned to slices because the slice by itself can't store the data:

// Allocates.
int[] a = [1, 2, 3];

// Does not allocate.
int[3] b = [1, 2, 3];

Enter fullscreen mode Exit fullscreen mode

The second is delegates that capture variables from an outer scope.
They allocate because the captured variables need to outlive the current stack frame:

// Allocates.
auto offset = 10;
auto a = (int x) => x + offset;

// Does not allocate.
auto b = (int x) => x + 10;
auto c = function(int x) => x + 10;

Enter fullscreen mode Exit fullscreen mode

The third is the ~ operator.
It allocates sometimes because it needs to create a new array at runtime:

// Allocates.
import std.conv;
auto a = "level_" ~ to!string(9);

// Does not allocate.
enum b = "level_" ~ to!string(9);

Enter fullscreen mode Exit fullscreen mode

One way to detect implicit allocations like the ones mentioned above is by using the -vgc flag:

dmd -vgc app.d
# Or: ldc2 --vgc app.d
# Or: gdc -ftransition=nogc app.d

Enter fullscreen mode Exit fullscreen mode

Example output:

[alex/Documents/code] dmd -vgc app.d 
app.d(31): vgc: array literal may cause a GC allocation
app.d(33): vgc: operator `~` may cause a GC allocation
app.d(28): vgc: using closure causes GC allocation

Enter fullscreen mode Exit fullscreen mode

For functions in precompiled code not covered by -vgc, it's usually easy to tell by reading the code.
Take writeln as an example: it might allocate if you pass a number (it doesn't, just assume), but it will not if you pass a string, since there is nothing to convert to a string.

writeln is also a great example of why you should not mark everything as @nogc. Its job is simply to print text. It shouldn't care whether the toString method of a type allocates with the GC or not.
That is a decision the caller has to make, and for templated functions the compiler can infer @nogc (can't with writeln because of exceptions).

Because @nogc is part of a function, it becomes an API contract the moment a function accepts a user-provided callback.
That is a strong constraint, so it should preferably appear where it genuinely makes sense: at API boundaries, or for self-contained functions like makeChars and toLevelName that never touch the GC.
A question to ask before using @nogc is whether this guarantee needs to be enforced by the compiler, or whether a comment could do a similar job.

A couple of additional notes worth mentioning:

Note 1: Debugging @nogc functions can be tricky since writeln is not allowed. A simple workaround is using debug statements:

void debugWriteln(A...)(A args) {
    import std.stdio;
    debug writeln(args);
}

@nogc
void myFunction() {
    debugWriteln("Something only for debugging.");
}

Enter fullscreen mode Exit fullscreen mode

Note 2: The -profile=gc flag can be used to create a profilegc.log file.
I never needed it personally, but from what I understand it tracks what allocated at runtime rather than what could allocate.

Conclusion

Manual memory management in D does not require @nogc or -betterC.
As shown above, you can do a lot with just the stack, static buffers, and a simple arena allocator.

A basic table of what and when to use from the discussed topics:

Situation Use
Temporary & self-contained Stack
Needs to outlive function Static buffer
Multiple callers & self-contained Passed memory
Many allocations or bulk free Arena

One last thing worth exploring is using MMM libraries like NuMem, Joka, or core.stdc.stdlib. NuMem is used in some D libraries, while Joka is a personal project of mine.