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
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] = 连续字节流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
auto shared = buf->clone();
// 只增加 sharedInfo_->refcount,不复制数据
// shared 和 buf 共享同一块内存SharedInfo and Custom Release
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>
| Dimension | IOBuf | vector<char> |
|---|---|---|
| Copy | Zero-copy (reference-counted) | O(n) deep copy |
| Splicing | O(1) linked-list operation | O(n) data move |
| Random access | Requires handling chain boundaries | O(1) |
| Use case | Network I/O, protocol stack | General-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 facility | IOBuf counterpart | Semantic difference |
|---|---|---|
std::vector<std::byte> | Contiguous byte buffer | vector 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 chunk | span 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_view | Text payload | IOBuf stores raw bytes with no null-termination guarantee; toString() requires an explicit call |
std::shared_ptr<T> | Shared ownership | shared_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 callunshare()first to ensure write safety. This differs from COWstd::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_isnullptrandisSharedOne()always returnstrue. 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)
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 withnew, released withdeletekHeapFullStorage: Allocated together with IOBuf + data; managed byHeapStoragewhen the reference count drops to zerokExtBuffer: Embedded at the tail of the data buffer (SharedInfoimmediately follows themalloc'd buffer)
Factory Functions
| Function | Semantics |
|---|---|
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
| Function | Semantics | Complexity |
|---|---|---|
appendToChain(unique_ptr) | Appends another chain to the tail of this one | O(1) pointer operations |
insertAfterThisOne(unique_ptr) | Inserts after the current node | O(1) |
unlink() | Removes self from the chain, returns unique_ptr | O(1) |
pop() | Removes and returns the rest of the chain excluding self | O(1) |
separateChain(head, tail) | Extracts the sub-chain [head, tail] | O(1) |
Sharing and Write Safety
| Function | Semantics |
|---|---|
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)
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: CallsfreeFn(buf_, userData)(supports mmap, external memory, etc.)freeFn == nullptr && userData != 0:sizedFree(buf_, size)(leveragesgoodMallocSizefor 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) <= 56is constrained byHeapStorage'sstatic_assert; field order and sizes are directly depended upon byHeapFullStorage(IOBuf + SharedInfo + data in a single allocation) andHeapPrefix(offsetof(HeapStorage, buf)).SharedInfo'sStorageTypeenum values and layout are directly used in pointer arithmetic withinreleaseStorage()(e.g.,reinterpret_castto compute theHeapFullStoragebase address).
Callback signatures:
FreeFunction = void(*)(void* buf, void* userData)is a C ABI-compatible function pointer; changing this signature would break alltakeOwnershipcallers.io_buf_alloc_cb/io_buf_free_cbare weak symbols whose signaturevoid(void*, size_t)is also defined by external programs.
operator new/delete overloads:
IOBuf::operator new(size_t)allocatesHeapStoragerather than a bareIOBuf;operator deletefalls back toHeapStorageviaoffsetof(HeapStorage, buf). Subclasses overriding new/delete would breakcreateCombined'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/appendChainare retained as forwarding wrappers.
Exception Safety
IOBuf's exception safety guarantees vary by operation:
Destructor and chain operations: noexcept
~IOBuf(),decrementRefcount(),freeExtBuffer()are all markednoexcept.FreeFunctiondocumentation requires "must not throw" — if a callback violates this contract,std::terminateis 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): IfSharedInfo'snewthrows, aScopeGuardcallsfreeFn(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(): Ifmallocfails, throwsbad_allocwith the original buffer unchanged.
Edge cases:
coalesceSlow(maxLength)throwsstd::overflow_errorifmaxLength > computeChainDataLength().checked_add/checkedMaththrowstd::bad_allocorstd::length_erroron overflow.unshareChained()callscoalesceSlow()if the chain contains shared chunks, which may fail withbad_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.
| Operation | data() pointer | Chunk iterator | Chain pointers (next()/prev()) | External sharedInfo_ |
|---|---|---|---|---|
cloneOne() / clone() | Unchanged | N/A | New chain is independent | Unchanged (refcount incremented) |
appendToChain(other) | Unchanged | Unchanged | All prev_ pointers on the original next_ chain are rewritten | Unchanged |
unlink() / pop() | Unchanged | Unchanged | Removed node's next_/prev_ point to itself; neighboring nodes in the original chain point to each other | Unchanged |
separateChain(h, t) | Unchanged | Unchanged | Pointers at the sub-chain endpoints and the break point in the original chain are modified | Unchanged |
coalesce() | Invalidated (new buffer) | All non-head nodes destroyed | Chain becomes a single node | Old sharedInfo_ decremented, new sharedInfo_ points to new buffer |
unshareOne() | Invalidated (new memcpy) | Unchanged | Unchanged | Old sharedInfo_ decremented |
unshare() (chained) | Invalidated (internally calls coalesceSlow) | All non-head nodes destroyed | Chain becomes a single node | Same as coalesce |
advance(n) / retreat(n) | Shifted (data_ += n) | Unchanged | Unchanged | Unchanged |
prepend(n) / trimStart(n) | Shifted (data_ -= n or += n) | Unchanged | Unchanged | Unchanged |
append(n) / trimEnd(n) | Unchanged (only length_ modified) | Unchanged | Unchanged | Unchanged |
trimWritableTail(n) | Unchanged (only capacity_ modified) | Unchanged | Unchanged | Unchanged |
reserve() | May be invalidated (reserveSlow reallocates) | Unchanged | Unchanged | Old sharedInfo_ may be replaced |
clear() | Reset to buffer() | Unchanged | Unchanged | Unchanged |
Key rules:
- After
coalesce()/unshare(), any previously saveddata()pointers,ByteRangeviews, oriovecarrays are all invalidated. - Chain operations (
appendToChain,unlink) do not invalidatedata()pointers, but do invalidate other node pointers saved by code that traverses the chain. IOBuf::Iteratoris a forward iterator and is necessarily invalidated aftercoalesce()/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).
| Scenario | Recommended strategy | Rationale |
|---|---|---|
| Forward directly after receiving from network | clone() + send | Fully zero-copy, optimal |
| Parse after receiving from network | gather(needed) partial coalesce | Only merge the first N bytes that require contiguous access |
| Send once after multiple appends | No coalesce; use getIov() + writev | Preserve chain structure, leverage scatter-gather I/O |
| Must pass to an API requiring contiguous memory | coalesce() 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
| Operation | Atomic instruction | Memory order |
|---|---|---|
cloneOne | fetch_add(1) | relaxed (lightest) |
decrementRefcount | load(acquire) + fetch_sub(1, acq_rel) | Heavier |
isSharedOne | load(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
FreeFunctionis a function-pointer call that cannot be inlined — one additional indirect call compared tofree(buf_).- The
takeOwnership(SIZED_FREE)path usesfolly::sizedFree(leveraginggoodMallocSizeto reduce jemalloc internal lookup), which is slightly faster than a barefree. HeapFullStoragemode (createCombined): IOBuf + SharedInfo + data in a single allocation/release pair, saving onemalloc/freecycle. 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:
| Dimension | IOBuf | std::vector<std::byte> | std::span<std::byte> | std::string (libstdc++) |
|---|---|---|---|---|
| Ownership | Reference-counted (sharable) | Exclusive (deep copy) | Non-owning view | COW (≤GCC 5) or exclusive |
| Memory layout | Chained chunks, each independently allocatable | Single contiguous block | No storage (pointer + size) | SSO + single contiguous block |
| Splicing | O(1) linked-list operation | O(n) copy | Not applicable | O(n) or COW |
| Zero-copy | clone() only increments refcount | Not supported | Ownership not involved | COW mode supports zero-copy |
| Thread safety | Refcount is atomic; IOBuf object itself is not locked | None | None | Refcount is atomic (COW) |
| Write guarantee | Caller must call unshare() first | Exclusive means writable | No write guarantee | COW mode performs implicit unshare |
| Use case | Network I/O, protocol stack, zero-copy pipeline | General-purpose byte container | Read-only/borrowed view | Text 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
#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/sKey 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).