← Back to Blog

Reference Counting and Smart Pointers in Memory Block Management

In high-performance network processing, memory blocks (like HTTP request bodies, response content) need to be passed between different modules. Using traditional copy methods incurs huge overhead, while using raw pointers makes memory management complex and prone to leaks. How to balance performance and safety?

This article provides an in-depth analysis of reference counting and smart pointer design in industrial-grade code, exploring how to achieve efficient memory management through intrusive design, with a clean-room reconstruction in Zig.

The Problem: Memory Management Dilemma

In typical network services, managing memory block lifecycle is challenging:

// Method 1: Copy
void process(char* data, size_t len) {
    char* copy = new char[len];
    memcpy(copy, data, len);
    // Process...
    delete[] copy;
}
// Drawback: Huge overhead from copying

// Method 2: Raw pointer
char* data = get_data();
// Who frees it? Easy to leak

Industrial Solution: Reference Counting + Smart Pointers

The original code uses a combined design:

class TChunkData: public TBigObj<TChunkData>, 
                 public TBlob::TBase, 
                 public TAtomicRefCount<TChunkData> {
    // Intrusive reference counting
};

The core ideas of this design:

  • Intrusive reference counting: Embed reference count directly in object, no extra metadata needed
  • Atomic operations: Use atomics for thread safety, avoiding lock overhead
  • Automatic release: Automatically free when reference count reaches zero

Trade-off Analysis

Advantages

  1. Zero metadata overhead: Reference count embedded in object header, no extra storage
  2. Lock-free path: Most cases don't require locking
  3. Automatic management: Free when reference count reaches zero

Costs

  1. Object constraints: Class must inherit from specific base classes
  2. Circular references: Need to manually break cycles
  3. Debugging difficulty: Reference counting issues are hard to trace

Zig Clean-Room Demonstration

const RefCount = struct {
    count: usize,

    pub fn init() RefCount {
        return RefCount{ .count = 1 };
    }

    pub fn ref(self: *RefCount) void {
        self.count += 1;
    }

    pub fn unref(self: *RefCount) bool {
        if (self.count > 0) self.count -= 1;
        return self.count == 0;
    }
};

const MemoryBlock = struct {
    ref_count: RefCount,
    data: []u8,
    
    pub fn ref(self: *MemoryBlock) void {
        self.ref_count.ref();
    }
    
    pub fn unref(self: *MemoryBlock) bool {
        return self.ref_count.unref();
    }
};

fn main() {
    var block = try MemoryBlock.create(allocator, 1024);
    block.ref();  // Add reference
    if (block.unref()) {
        // Free memory
    }
}

Summary

This article provides an in-depth analysis of reference counting and smart pointer design in memory block management, exploring the following core trade-offs:

  1. Intrusive vs Non-intrusive: Trade object constraints for zero metadata overhead
  2. Atomic operations vs Locks: Trade CPU cache misses for lock-free paths
  3. Automatic vs Manual management: Trade debugging difficulty for development efficiency

This design pattern is very common in systems requiring high-performance memory management. Understanding the trade-offs behind it is crucial for designing reliable systems.