absl::Cord — 분산 시스템용 대용량 문자열
#한 줄 요약
absl::Cord는 큰 문자열을 조각으로 표현하는 자료구조다. 내부적으로 leaf chunk의 reference-counted tree를 유지해 concat·substring·prepend가 O(log n)에서 복사 없이 가능하다. Google 내부 RPC payload, file IO buffer, log message 누적 등에 표준으로 쓰인다.
#동기
수십 MB의 문자열을 다루다 보면 std::string의 한계가 드러난다.
// 회피 — 매 concat마다 전체 복사std::string body;for (auto& chunk : chunks) { body += chunk; // O(n^2) 누적 복사}
// 회피 — substring이 매번 복사std::string head = body.substr(0, 1024);string_view로 substring을 피할 수 있지만 원본 lifetime에 묶여서 RPC 응답이나 비동기 처리에는 쓰기 어렵다.
Cord는 두 가지 목표를 모두 만족한다.
- shared ownership — chunk는 reference count로 공유. substring·copy가 zero-copy.
- balanced tree — concat이 O(log n), 매우 큰 데이터에서도 일정 비용.
내부 tree 구조를 그림으로 보면 다음과 같다.
#메모리 모델 — arena가 왜 어울리나
Cord는 작은 chunk 노드를 수없이 만들고 짧은 lifetime에 모았다 푼다. 이 패턴은 일반 malloc/free보다 arena allocator가 훨씬 잘 맞는다.
bump pointer로 O(1) 할당, request 끝에 통째로 reset — per-object 헤더와 단편화가 사라진다. Cord의 CordRep 노드, IOBuf의 buffer 메타데이터, 임시 parser AST 등이 모두 동일한 동기로 arena를 쓴다.
#데이터 모델
Cord는 두 가지 표현을 가진다.
Inline (≤15 byte): SSO처럼 객체 안에 직접 저장. 작은 문자열에 alloc 없음.
Tree: 16 byte 이상이면 CordRep tree로 전환.
Cord = "Hello, " + 1MB chunk + ", world"
CONCAT / \ CONCAT FLAT("...world") / \ FLAT EXTERNAL(1MB)("Hello,") (refcount=2)leaf 타입:
FLAT— 작은 inline byte 배열 (4KB까지)EXTERNAL— 외부 메모리의 view + releaserSUBSTRING— 다른 chunk의 일부BTREE— 자식 노드 묶음 (default), 또는CONCAT(legacy)
각 leaf는 refcount를 가진다. Cord c2 = c1;은 root에서 refcount만 증가시킨다.
#API와 사용법
#include "absl/strings/cord.h"
absl::Cord c("Hello");c.Append(", world!"); // O(log n), 새 chunk 한 개 추가c.Prepend("Greeting: "); // O(log n)
absl::Cord copy = c; // O(1), refcount 증가absl::Cord sub = c.Subcord(9, 20); // O(log n), zero-copy
// 반복for (absl::string_view chunk : c.Chunks()) { ::write(fd, chunk.data(), chunk.size());}
// 평탄화 — std::string 필요 시 (alloc)std::string flat(c);absl::string_view view = c.Flatten(); // 내부적으로 평탄화
// 외부 메모리 흡수absl::Cord c = absl::MakeCordFromExternal(view, [](absl::string_view) { // releaser — refcount 0 시 호출});Chunks()는 leaf를 순회한다. 한 chunk가 string_view이므로 writev(2) scatter-gather IO와 자연스럽게 결합한다.
#내부 구현 핵심
absl/strings/cord.h와 cord_internal.h에서 발췌.
class Cord { public: Cord() = default; Cord(absl::string_view src); Cord(const Cord& src); // O(1) — refcount 증가 Cord(Cord&& src) noexcept;
void Append(absl::string_view src); void Append(Cord src); void Prepend(absl::string_view src);
Cord Subcord(size_t pos, size_t n) const; // O(log n)
size_t size() const; bool empty() const;
ChunkRange Chunks() const; CharIterator char_begin() const;
std::string ToString() const; absl::string_view Flatten();
private: // InlineRep — union { inline_buf[15]; CordRep* tree; } cord_internal::InlineData contents_;};#InlineData 트릭
InlineData는 16 byte 객체에 두 형태를 인코딩한다.
struct InlineData { // Inline mode — 마지막 byte의 LSB = 0 char inline_buf[15]; uint8_t tagged_size; // bit0=0 → inline, bits1-7 = size
// Tree mode — 마지막 byte의 LSB = 1 // CordRep* tree (8B) + reserved (7B) + tag (1B, LSB=1)};LSB 하나로 inline/tree를 구분한다. 작은 문자열은 alloc 없이 객체 안에 산다.
#CordRep refcount
struct CordRep { std::atomic<int32_t> refcount; uint8_t tag; // FLAT / EXTERNAL / SUBSTRING / BTREE uint8_t storage[3]; size_t length;};tag로 실제 타입을 알아낸다. C-style discriminated union이다.
#Btree 균형
옛 구현은 CONCAT 노드의 binary tree였다. 현재는 BTREE 노드(branching factor ~64)로 변경되어 깊이가 매우 얕다. 64-bit 빌드에서 4GB 문자열도 깊이 5 이내.
Btree root (height=2)├─ Btree (height=1)│ ├─ FLAT [0..4KB]│ ├─ FLAT [4KB..8KB]│ ├─ ...│ └─ FLAT (~64개)├─ Btree (height=1)└─ ...Append는 가장 오른쪽 leaf를 채우거나 새 leaf를 매단다. O(log n) 안에 끝난다.
#std::string과의 비교
| 항목 | std::string | absl::Cord |
|---|---|---|
| 메모리 모델 | 연속 buffer | refcount tree |
| inline (SSO) | 15B (libstdc++ x86_64) | 15B |
| concat | O(n) copy | O(log n) tree op |
| substring | O(n) alloc + copy | O(log n) zero-copy |
| copy | O(n) | O(1) refcount |
임의 접근 s[i] | O(1) | O(log n) |
data() 연속 buffer | O(1) | Flatten() 비용 발생 |
| iterator stable | O(1) increment | CharIterator O(log n) |
| 스레드 안전 | const-share OK | const-share OK (refcount atomic) |
핵심 트레이드오프는 random access 비용이다. byte 단위 인덱싱이 잦으면 Cord는 손해. 반대로 큰 buffer를 전달·잘라내기·합치기가 잦으면 Cord가 압도적.
#활용 — RPC payload
Google 내부 RPC framework (Stubby, gRPC++의 일부)에서 응답 body가 Cord로 흐른다.
// 가상 RPC 핸들러absl::Status Handle(const Request& req, Response* res) { absl::Cord body; body.Append(BuildHeader(req)); // 작은 inline body.Append(LoadFromDisk(req.key)); // 큰 file mmap을 EXTERNAL chunk로 body.Append(BuildFooter());
*res->mutable_payload() = std::move(body); return absl::OkStatus();}LoadFromDisk는 MakeCordFromExternal로 mmap 영역을 복사 없이 흡수한다. RPC 직렬화도 Chunks() 순회 + scatter-gather write로 끝난다.
#코드 리뷰 포인트
1. 크기 기준으로 Cord 적용
| 조건 | 선택 |
|---|---|
| < 4KB | std::string 또는 string_view |
| 4KB ~ 64KB | 케이스 바이 케이스 |
| > 64KB | Cord 강력 검토 |
| mutable 잦음 | std::string |
| shared 잦음 | Cord |
작은 문자열에 Cord를 쓰면 tree overhead만 산다.
2. Flatten 남용 금지
// 회피 — Cord 잘라서 std::string으로std::string head(cord.Subcord(0, 1024));
// Good — Cord 그대로 처리absl::Cord head = cord.Subcord(0, 1024);Flatten/ToString은 모든 chunk를 연속 buffer로 복사한다. Cord 전체의 의미가 사라진다.
3. data() 가정 금지
absl::string_view와 달리 Cord는 내부 buffer가 연속이 아니다. data()가 없다. 연속 buffer가 필요한 C API에는 Flatten() 후 view를 전달하거나 chunk 단위로 보낸다.
// 회피::write(fd, cord.data(), cord.size()); // 컴파일 에러
// Good — scatter-gatherstd::vector<iovec> iov;for (absl::string_view ch : cord.Chunks()) { iov.push_back({const_cast<char*>(ch.data()), ch.size()});}::writev(fd, iov.data(), iov.size());4. random access 회피
// 회피 — O(n log n)for (size_t i = 0; i < cord.size(); ++i) { Use(cord[i]);}
// Good — chunk 순회 O(n)for (absl::string_view ch : cord.Chunks()) { for (char c : ch) Use(c);}#자주 보는 안티패턴
작은 문자열에 Cord
Cord("hi")처럼 짧은 문자열에 Cord를 쓰면 inline 영역으로 들어가지만 *함수 시그니처가 const Cord&*면 호출 측에서 임시 Cord 객체를 만들어야 한다. 사실상 손해.
mutable string처럼 사용
// 회피 — 빈번한 mutationCord buf;for (int i = 0; i < 1000000; ++i) { buf.Append(absl::StrCat(i, "\n"));}작은 chunk를 백만 번 추가하면 tree node가 많아지고 cache 영향 큼. std::string + 일정 시점에 Cord로 흡수가 낫다.
Cord 비교를 == 외의 ordering으로
Cord도 operator<를 지원하지만 chunk-by-chunk 비교라 비용이 인접 string_view보다 무겁다. 정렬 키로는 std::string이 일반적으로 더 적합.
#정리
absl::Cord는 chunk tree로 구성된 공유 가능한 문자열.- concat/substring/copy가 모두 O(log n) 또는 O(1).
- random access·
data()연속 buffer가 약점. - RPC payload, log buffer, mmap 흡수처럼 큰 데이터 공유에 최적.
- 작은 문자열에는 overhead만 산다.
#다음 편
Part 15-02 — charconv에서 빠른 숫자 변환을 본다.
#관련 항목
- Part 15-02 — charconv
- Part 15-03 — Cord vs std::string 선택
- Part 4-01 — string_view
- Part 4-03 — StrCat
- Folly Part 5-02 — IOBuf — Meta의 zero-copy buffer chain
Abseil Code Review · 74 of 79
- 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
- 2 Abseil 개요 — Google이 std를 보완한 이유
- 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
- 4 Abseil 빌드와 의존성 — Bazel vs CMake
- 5 Abseil LTS vs HEAD 릴리스 모델 분석
- 6 Abseil Versioning과 ABI 호환성 정책
- 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
- 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
- 9 absl::LogSeverity — 로그 레벨 타입
- 10 Abseil type_traits — negation·conjunction·void_t
- 11 Abseil Conformance·Policy 분석
- 12 Abseil Memory utilities 분석
- 13 Abseil raw_logging — heap-free 로깅
- 14 Abseil thread_annotations — clang TSA 통합
- 15 absl::Status — exception-free error handling
- 16 absl::StatusOr<T> — 값 또는 에러
- 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
- 18 absl::Status payload — 구조화된 에러 컨텍스트
- 19 absl::Status ↔ exception 변환 패턴
- 20 absl::string_view — non-owning 문자열 참조
- 21 absl::string_view 함정 — dangling·c_str·임시 객체
- 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
- 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
- 24 absl::StrJoin — 컨테이너 결합과 Formatter
- 25 absl::StrFormat — type-safe printf·FormatSpec
- 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
- 27 Abseil Escape — CEscape·HexEscape·Base64
- 28 absl::flat_hash_map — Swiss Table 기반 hash map
- 29 absl::flat_hash_set — set 버전 Swiss Table
- 30 absl::node_hash_map — stable pointer가 필요할 때
- 31 absl::btree_map — sorted·cache-friendly B-tree
- 32 absl::FixedArray — 런타임 크기 stack 배열
- 33 absl::InlinedVector — small buffer optimization
- 34 Abseil Swiss Table internals — control byte·SIMD probing
- 35 absl::Mutex — reader-writer·fairness·deadlock 검출
- 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
- 37 absl::Notification — once-only signal
- 38 absl::BlockingCounter·Barrier — 다중 thread 조율
- 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
- 40 absl::Time·Duration 분석 — 단단한 type
- 41 absl::Time Format·Parse
- 42 absl::CivilTime 분석
- 43 absl::time_zone 분석
- 44 absl::Time mocking — 테스트 친화 시간
- 45 absl::BitGen — 모던 난수 생성기
- 46 Abseil Random Distributions — Uniform·Exponential
- 47 Abseil Mocking Random — 테스트 결정성
- 48 Abseil Random Seeding·Entropy
- 49 absl::int128·uint128 분석
- 50 absl::bits — popcount·countl_zero
- 51 absl::optional vs std::optional
- 52 absl::variant 분석
- 53 absl::span 분석
- 54 absl::any 분석
- 55 absl::compare — three-way 비교
- 56 Abseil utility — apply·in_place
- 57 Abseil AbslHashValue 분석
- 58 Abseil HashState chaining
- 59 Abseil Custom hashable 구현
- 60 Abseil LOG·VLOG·CHECK 분석
- 61 Abseil LogSink 분석
- 62 Abseil LogEntry·structured logging
- 63 Abseil Stack trace·failure_signal_handler
- 64 ABSL_FLAG 정의 분석
- 65 Abseil ParseCommandLine 동작
- 66 Abseil Flag introspection·validation
- 67 Google 스타일의 Abseil 사용 패턴
- 68 Abseil 자주 보는 anti-pattern
- 69 std → absl 마이그레이션 전략
- 70 absl::Cleanup — 함수 종료 시 실행 보장
- 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
- 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
- 73 absl::bind_front와 Overload — 함수 객체 보조 도구
- 74 absl::Cord — 분산 시스템용 대용량 문자열
- 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
- 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
- 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
- 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
- 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피
관련 글
absl::Cord vs std::string — 선택 기준과 메모리 프로파일
absl::Cord와 std::string 중 무엇을 쓸지 판단하는 기준 — 크기·mutation 패턴·공유 빈도·메모리 프로파일 비교.
같은 시리즈에서 이어 읽기
Abseil Escape — CEscape·HexEscape·Base64
Part 4-08: absl::CEscape / CHexEscape / Base64Escape / WebSafeBase64Escape — 안전한 문자열 이스케이프와 base64 인코딩의 모든 변형.
같은 시리즈에서 이어 읽기
Abseil ASCII 함수 — locale-free 분류·대소문자 변환
Part 4-07: absl::ascii_* — locale 독립 ASCII 분류, AsciiStrToLower / AsciiStrToUpper / StripAsciiWhitespace.
같은 시리즈에서 이어 읽기