Skip to content

Folly IOBuf: Zero-Copy Chained Buffer

Source path: references/impl/folly/folly/IOBuf.h

IOBuf is the data backbone of Meta's networking stack (proxygen) — a zero-copy, reference-counted, chained buffer.

Internal Structure

cpp
class IOBuf {
  IOBuf* next_;     // 循环双向链表
  IOBuf* prev_;
  uint8_t* data_;   // 数据指针
  size_t length_;   // 有效数据长度
  size_t capacity_; // 缓冲区容量
  SharedInfo* sharedInfo_;  // 引用计数 + 释放回调
};

Chained Buffers

IOBuf 循环双向链表(buf1->appendChain(buf2->appendChain(buf3))):

  ┌──────┐   next_   ┌──────┐   next_   ┌──────┐   next_
  │ buf1 │ ────────→ │ buf2 │ ────────→ │ buf3 │ ────────→ buf1(循环)
  │d[0..1K]          │d[0..512]         │d[0..256]
  └──────┘           └──────┘           └──────┘

  逻辑上:buf1.data[0..1023] + buf2.data[0..511] + buf3.data[0..255] = 连续字节流
cpp
auto buf1 = IOBuf::create(1024);
auto buf2 = IOBuf::create(512);
buf1->appendChain(std::move(buf2));
// buf1 -> buf2 -> buf1 (循环链表)

for (auto& chunk : *buf1) {
  process(chunk.data(), chunk.length());
}

Zero-Copy Clone

cpp
auto shared = buf->clone();
// 只增加 sharedInfo_->refcount,不复制数据
// shared 和 buf 共享同一块内存

SharedInfo and Custom Release

cpp
struct SharedInfo {
  std::atomic<size_t> refcount;
  FreeFunction freeFn;    // 自定义释放函数
  void* userData;
};
// 允许 IOBuf 管理任意来源的内存:
// - 堆分配(freeFn = free)
// - mmap 映射(freeFn = munmap)
// - 外部只读内存(freeFn = no-op)

Comparison with std::vector<char>

DimensionIOBufvector<char>
CopyZero-copy (reference-counted)O(n) deep copy
SplicingO(1) linked-list operationO(n) data move
Random accessRequires handling chain boundariesO(1)
Use caseNetwork I/O, protocol stackGeneral-purpose byte buffer

User API

Users typically interact with IOBuf through IOBuf::create, appendChain, clone, coalesce, data()/length(), and similar entry points; the preceding sections have already explained chained buffers and zero-copy clone.

Standard Semantics

IOBuf is not a standard library type and does not satisfy any standard concept such as std::ranges::range, std::contiguous_iterator, or TriviallyCopyable. Its semantic boundaries with standard facilities are as follows:

Standard facilityIOBuf counterpartSemantic difference
std::vector<std::byte>Contiguous byte buffervector owns memory and deep-copies; IOBuf uses reference-counted zero-copy, and only the [data(), tail()) range within a single chunk is contiguous
std::span<std::byte>Non-owning view of a single chunkspan is a trivial view with compile-time or dynamic size; IOBuf additionally carries headroom/tailroom, linked-list structure, and reference counting
std::string / std::string_viewText payloadIOBuf stores raw bytes with no null-termination guarantee; toString() requires an explicit call
std::shared_ptr<T>Shared ownershipshared_ptr controls object lifetime; IOBuf's reference count only governs the underlying buffer — the IOBuf object itself has no intrusive reference count

Key semantic constraints:

  • Must unshare before writing: IOBuf's writableData() does not check exclusivity — callers must call unshare() first to ensure write safety. This differs from COW std::string (which performs an implicit fork).
  • Opaque chain structure: Before coalesce(), a logically contiguous byte stream may be distributed across multiple non-contiguous chunks that standard algorithms cannot directly traverse.
  • No ownership semantics: An IOBuf created by wrapBuffer() does not own the underlying memory; sharedInfo_ is nullptr and isSharedOne() always returns true. Callers must ensure the buffer remains valid until all IOBuf instances are destroyed.

Object Layout

The IOBuf header fields and circular doubly-linked list have been shown above; a layout diagram for SharedInfo, headroom/tailroom, and external buffer wrapping will be added later.

Core Source Paths

The IOBuf.h path was given at the top of this document; paths for IOBuf.cpp, takeOwnership, coalesce, cloneOne, and other entry functions will be added later.

Core Classes / Functions

IOBuf (IOBuf.h:292)

Primary type, 56 bytes (sizeof(IOBuf) <= 56 guaranteed by HeapStorage's static_assert).

偏移    字段             类型              说明
─────────────────────────────────────────────────────
 0      length_          size_t            有效数据长度
 8      data_            uint8_t*          数据起始指针
16      capacity_        size_t            缓冲区总容量
24      buf_             uint8_t*          缓冲区起始指针
32      next_            IOBuf*            链表后继(循环,永不为 null)
40      prev_            IOBuf*            链表前驱(循环,永不为 null)
48      sharedInfo_      SharedInfo*       引用计数块(可能为 null)

SharedInfo (IOBuf.h:2120)

cpp
struct SharedInfo {
  FreeFunction freeFn{nullptr};           // 自定义释放回调
  void* userData{nullptr};                // 回调用户数据
  SharedInfoObserverEntryBase* observerListHead{nullptr};
  std::atomic<uint32_t> refcount{1};      // 原子引用计数
  bool externallyShared{false};           // 标记外部共享
  StorageType storageType = kInvalid;     // 存储类型枚举
  MicroSpinLock observerListLock{0};      // observer 链表锁
};

The StorageType enum determines how SharedInfo itself is released:

  • kAllocated: Independently allocated with new, released with delete
  • kHeapFullStorage: Allocated together with IOBuf + data; managed by HeapStorage when the reference count drops to zero
  • kExtBuffer: Embedded at the tail of the data buffer (SharedInfo immediately follows the malloc'd buffer)

Factory Functions

FunctionSemantics
create(capacity)≤1024 bytes uses createCombined (IOBuf+SharedInfo+data in a single allocation); otherwise createSeparate
createCombined(capacity)Single malloc: HeapFullStorage + data; static_assert(sizeof(HeapFullStorage) <= 64)
createSeparate(capacity)IOBuf object and data buffer allocated separately
createChain(total, max)Creates a chained IOBuf, each chunk ≤ max
takeOwnership(buf, cap, ...)Takes ownership of an externally allocated buffer, with a specified FreeFunction
wrapBuffer(buf, cap)Non-owning wrapper; sharedInfo_ = nullptr, does not manage memory
copyBuffer(data, size, ...)Deep copy: create + memcpy

Chain Operations

FunctionSemanticsComplexity
appendToChain(unique_ptr)Appends another chain to the tail of this oneO(1) pointer operations
insertAfterThisOne(unique_ptr)Inserts after the current nodeO(1)
unlink()Removes self from the chain, returns unique_ptrO(1)
pop()Removes and returns the rest of the chain excluding selfO(1)
separateChain(head, tail)Extracts the sub-chain [head, tail]O(1)

Sharing and Write Safety

FunctionSemantics
clone()Traverses the chain, calling cloneOne (fetch_add(1)) on each chunk
cloneOne()Current chunk only: refcount.fetch_add(1, relaxed) + creates a new IOBuf object
unshare()If the chain contains shared chunks, coalesceSlow() merges them into a contiguous buffer
unshareOne()Current chunk only: allocates a new buffer + memcpy + decrements the old reference count
isSharedOne()!sharedInfo_ || externallyShared || refcount > 1
coalesce()Merges the entire chain into a single contiguous buffer, returns ByteRange

Key Algorithms

1. Chain Splicing (appendToChain, IOBuf.cpp:774)

链 A: [A1]⇄[A2]⇄[A1]    链 B: [B1]⇄[B2]⇄[B1]

appendToChain(B1) 后:
  A1.prev_→A2.next_→B1.next_→B2.next_→A1(循环)

  关键操作:4 次指针赋值,无数据拷贝
  A2.next_ = B1;  B1.prev_ = A2;
  B2.next_ = A1;  A1.prev_ = B2;

2. Zero-Copy Clone (cloneOneImpl, IOBuf.cpp:802)

cpp
unique_ptr<IOBuf> IOBuf::cloneOneImpl(mr) const {
  if (sharedInfo_) {
    sharedInfo_->refcount.fetch_add(1, std::memory_order_relaxed);
  }
  // 只分配新的 IOBuf 对象(HeapStorage),共享同一缓冲区
  return unique_ptr<IOBuf>{new (&storage->buf) IOBuf(
      InternalConstructor(), sharedInfo_, buf_, capacity_, data_, length_)};
}

clone() traverses each chunk in the chain, calls cloneOneImpl, then splices them together with appendToChain.

3. coalesce (coalesceAndReallocate, IOBuf.cpp:1041)

coalesce 前:  [Buf1:head|data1|tail] → [Buf2:head|data2|tail] → [Buf3:head|data3|tail]

coalesce 后:  [head₁|data1+data2+data3|tail₃]  单一连续缓冲

步骤:
1. 计算 newLength = Σ length_i
2. allocExtBuffer(newHeadroom + newLength + newTailroom)
3. 从 this 开始遍历链,memcpy 每个 chunk 的数据到新缓冲区
4. decrementRefcount()(旧缓冲区)
5. separateChain(next_, end->prev_)(摘除旧节点,立即析构)

4. Reference Count Release (decrementRefcount, IOBuf.cpp:1088)

decrementRefcount():
  if (!sharedInfo_) return;                    // wrapBuffer 的 IOBuf 无 refcount
  if (refcount.load(acquire) > 1) {            // 快速路径:非最后引用
    if (fetch_sub(1, acq_rel) > 1) return;     // 还有其他引用,直接返回
  }
  // 最后一个引用
  freeExtBuffer();                             // 调用 freeFn(buf_, userData) 或 free(buf_)
  SharedInfo::releaseStorage(this, type, info); // 释放 SharedInfo 自身

freeExtBuffer selects the release method based on whether freeFn is set:

  • freeFn != nullptr: Calls freeFn(buf_, userData) (supports mmap, external memory, etc.)
  • freeFn == nullptr && userData != 0: sizedFree(buf_, size) (leverages goodMallocSize for faster lookup)
  • Otherwise: free(buf_)

ABI Constraints

IOBuf does not provide standard-library-style ABI guarantees. Its ABI constraints originate from:

Object layout coupling:

  • sizeof(IOBuf) <= 56 is constrained by HeapStorage's static_assert; field order and sizes are directly depended upon by HeapFullStorage (IOBuf + SharedInfo + data in a single allocation) and HeapPrefix (offsetof(HeapStorage, buf)).
  • SharedInfo's StorageType enum values and layout are directly used in pointer arithmetic within releaseStorage() (e.g., reinterpret_cast to compute the HeapFullStorage base address).

Callback signatures:

  • FreeFunction = void(*)(void* buf, void* userData) is a C ABI-compatible function pointer; changing this signature would break all takeOwnership callers.
  • io_buf_alloc_cb / io_buf_free_cb are weak symbols whose signature void(void*, size_t) is also defined by external programs.

operator new/delete overloads:

  • IOBuf::operator new(size_t) allocates HeapStorage rather than a bare IOBuf; operator delete falls back to HeapStorage via offsetof(HeapStorage, buf). Subclasses overriding new/delete would break createCombined's assumptions.

API evolution strategy:

  • Folly targets source compatibility and does not guarantee cross-version binary compatibility.
  • New factory functions (e.g., cloneCoalescedAsValue) and PMR overloads are the primary evolution mechanisms.
  • Deprecated prependChain / appendChain are retained as forwarding wrappers.

Exception Safety

IOBuf's exception safety guarantees vary by operation:

Destructor and chain operations: noexcept

  • ~IOBuf(), decrementRefcount(), freeExtBuffer() are all marked noexcept.
  • FreeFunction documentation requires "must not throw" — if a callback violates this contract, std::terminate is called.
  • Move constructor and move assignment are both noexcept.

Allocation failure: throws std::bad_alloc, strong exception guarantee

  • create(), createCombined(), createSeparate(), copyBuffer(): Allocation failure throws directly with no side effects.
  • takeOwnership(buf, cap, freeFn, userData, freeOnError=true): If SharedInfo's new throws, a ScopeGuard calls freeFn(buf, userData) to roll back — i.e., strong exception guarantee (IOBuf.cpp:500).
  • coalesce(), coalesceAndReallocate(): If the new buffer allocation fails, the original chain state is unchanged — strong exception guarantee.
  • reserveSlow(): If malloc fails, throws bad_alloc with the original buffer unchanged.

Edge cases:

  • coalesceSlow(maxLength) throws std::overflow_error if maxLength > computeChainDataLength().
  • checked_add / checkedMath throw std::bad_alloc or std::length_error on overflow.
  • unshareChained() calls coalesceSlow() if the chain contains shared chunks, which may fail with bad_alloc — the original chain remains unchanged.

Iterator / Reference Invalidation

IOBuf's invalidation rules differ from std::vector — there is no unified "all iterators invalidated" semantics because the chain structure and buffer lifetimes are decoupled.

Operationdata() pointerChunk iteratorChain pointers (next()/prev())External sharedInfo_
cloneOne() / clone()UnchangedN/ANew chain is independentUnchanged (refcount incremented)
appendToChain(other)UnchangedUnchangedAll prev_ pointers on the original next_ chain are rewrittenUnchanged
unlink() / pop()UnchangedUnchangedRemoved node's next_/prev_ point to itself; neighboring nodes in the original chain point to each otherUnchanged
separateChain(h, t)UnchangedUnchangedPointers at the sub-chain endpoints and the break point in the original chain are modifiedUnchanged
coalesce()Invalidated (new buffer)All non-head nodes destroyedChain becomes a single nodeOld sharedInfo_ decremented, new sharedInfo_ points to new buffer
unshareOne()Invalidated (new memcpy)UnchangedUnchangedOld sharedInfo_ decremented
unshare() (chained)Invalidated (internally calls coalesceSlow)All non-head nodes destroyedChain becomes a single nodeSame as coalesce
advance(n) / retreat(n)Shifted (data_ += n)UnchangedUnchangedUnchanged
prepend(n) / trimStart(n)Shifted (data_ -= n or += n)UnchangedUnchangedUnchanged
append(n) / trimEnd(n)Unchanged (only length_ modified)UnchangedUnchangedUnchanged
trimWritableTail(n)Unchanged (only capacity_ modified)UnchangedUnchangedUnchanged
reserve()May be invalidated (reserveSlow reallocates)UnchangedUnchangedOld sharedInfo_ may be replaced
clear()Reset to buffer()UnchangedUnchangedUnchanged

Key rules:

  • After coalesce() / unshare(), any previously saved data() pointers, ByteRange views, or iovec arrays are all invalidated.
  • Chain operations (appendToChain, unlink) do not invalidate data() pointers, but do invalidate other node pointers saved by code that traverses the chain.
  • IOBuf::Iterator is a forward iterator and is necessarily invalidated after coalesce() / unshare() (chained).

Performance Model

Zero-Copy Sharing vs. Eventual Coalesce

IOBuf's core trade-off: defer data copying to achieve zero-copy performance on the I/O path, but eventually coalesce when consuming data (e.g., parsing protocol headers).

ScenarioRecommended strategyRationale
Forward directly after receiving from networkclone() + sendFully zero-copy, optimal
Parse after receiving from networkgather(needed) partial coalesceOnly merge the first N bytes that require contiguous access
Send once after multiple appendsNo coalesce; use getIov() + writevPreserve chain structure, leverage scatter-gather I/O
Must pass to an API requiring contiguous memorycoalesce() or cloneCoalesced()Unavoidable copy

Chain Length Overhead

  • Traversal overhead: computeChainDataLength(), countChainElements() are both O(N), where N is the chain length.
  • Cache locality: Each IOBuf object is independently heap-allocated (HeapStorage); different chunks in a chain are typically non-adjacent, causing L1/L2 cache misses during traversal. Longer chains produce more misses.
  • coalesce memcpy cost: Linear in total data size. For a chain of 10 × 1KB chunks, coalesce requires memcpy of ~10KB of data + allocation of a new buffer.

Atomic Operation Overhead of Reference Counting

OperationAtomic instructionMemory order
cloneOnefetch_add(1)relaxed (lightest)
decrementRefcountload(acquire) + fetch_sub(1, acq_rel)Heavier
isSharedOneload(acquire)Lightweight read

Fast-path optimization: decrementRefcount first uses load(acquire) to check refcount > 1, and only executes fetch_sub when it is not the last reference — avoiding the acq_rel overhead in non-shared scenarios.

Custom Release Callback Overhead

  • FreeFunction is a function-pointer call that cannot be inlined — one additional indirect call compared to free(buf_).
  • The takeOwnership(SIZED_FREE) path uses folly::sizedFree (leveraging goodMallocSize to reduce jemalloc internal lookup), which is slightly faster than a bare free.
  • HeapFullStorage mode (createCombined): IOBuf + SharedInfo + data in a single allocation/release pair, saving one malloc/free cycle. This is the default behavior for small buffers ≤1024 bytes.

libstdc++ vs libc++ vs MSVC

IOBuf is a Folly-proprietary type and does not involve standard library implementation differences. The following compares its design trade-offs against standard library buffer facilities:

DimensionIOBufstd::vector<std::byte>std::span<std::byte>std::string (libstdc++)
OwnershipReference-counted (sharable)Exclusive (deep copy)Non-owning viewCOW (≤GCC 5) or exclusive
Memory layoutChained chunks, each independently allocatableSingle contiguous blockNo storage (pointer + size)SSO + single contiguous block
SplicingO(1) linked-list operationO(n) copyNot applicableO(n) or COW
Zero-copyclone() only increments refcountNot supportedOwnership not involvedCOW mode supports zero-copy
Thread safetyRefcount is atomic; IOBuf object itself is not lockedNoneNoneRefcount is atomic (COW)
Write guaranteeCaller must call unshare() firstExclusive means writableNo write guaranteeCOW mode performs implicit unshare
Use caseNetwork I/O, protocol stack, zero-copy pipelineGeneral-purpose byte containerRead-only/borrowed viewText string

Standard library implementation differences do not affect IOBuf — IOBuf customizes operator new/delete, memory allocation, and reference counting, completely bypassing standard library allocator and container semantics. IOBuf's behavior is consistent across platforms (Linux/macOS/Windows); differences are limited to the underlying malloc implementation (jemalloc vs tcmalloc vs system malloc) and its support for Folly memory utilities such as goodMallocSize / xallocx.

Minimal Reproduction Code

cpp
#include <folly/io/IOBuf.h>

int main() {
  auto buf = folly::IOBuf::create(128);
  auto copy = buf->clone();
  return copy->capacity() > 0 ? 0 : 1;
}

Compile / Disassembly / Benchmark Evidence

Benchmark Results (source: folly/io/test/IOBufBenchmark.cpp)

The following data comes from folly::runBenchmarks with --bm_min_iters 100000:

操作                                 时间/iter     吞吐量
──────────────────────────────────────────────────────────
createAndDestroy (10B)               17.42 ns     57.41 M/s
cloneOne                             23.73 ns     42.14 M/s
cloneOneInto (避免堆分配)            19.08 ns     52.40 M/s
clone (链式)                         24.92 ns     40.13 M/s
cloneInto (避免堆分配)               21.74 ns     45.99 M/s
move                                 8.61 ns     116.17 M/s
copy (拷贝构造=clone)                21.23 ns     47.11 M/s
cloneCoalescedBaseline               201.31 ns     4.97 M/s
  (clone + coalesce 分开)
cloneCoalesced (优化版)              36.21 ns     27.62 M/s  ← 5.56× 快
takeOwnership                        36.01 ns     27.77 M/s
──────────────────────────────────────────────────────────
createAndDestroyMulti(64B)           32.74 μs     30.54 K/s
createAndDestroyMulti(4096B)         70.16 μs     14.25 K/s
createAndDestroyMulti(10240B)        83.84 μs     11.93 K/s
createAndDestroyMulti(16384B)        93.03 μs     10.75 K/s

Key Findings

Advantage of zero-copy clone: cloneOne (24 ns) vs copyBuffer (requires malloc + memcpy, approximately 30-50 ns depending on data size). For a typical 1500-byte network MTU packet, zero-copy saves approximately 50-100 ns/packet.

cloneCoalesced vs clone + coalesce: The optimized cloneCoalescedAsValue (36 ns) is 5.56× faster than the baseline (201 ns) — the reason is that it avoids intermediate chain heap allocations (each cloneOne requires a HeapStorage allocation). This gap grows with longer chains.

cloneOneInto vs cloneOne: Reusing a stack-allocated IOBuf object (19 ns vs 24 ns) saves one HeapStorage allocation/release cycle. Worth using in hot paths.

createAndDestroyMulti allocator jump: There is a noticeable latency jump from 4096B to 5120B (70 μs → 80 μs), corresponding to jemalloc's size-class boundary (4KB slab → 8KB slab).

Released under the MIT License