본문으로 건너뛰기
Abseil Code Review · 73/79

absl::Cord — 분산 시스템용 대용량 문자열

· Hawk · 5분 읽기

#한 줄 요약

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 구조를 그림으로 보면 다음과 같다.

Cord 불변 chunk tree

#메모리 모델 — arena가 왜 어울리나

Cord는 작은 chunk 노드를 수없이 만들고 짧은 lifetime에 모았다 푼다. 이 패턴은 일반 malloc/free보다 arena allocator가 훨씬 잘 맞는다.

Arena allocator userspace

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 + releaser
  • SUBSTRING — 다른 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.hcord_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::stringabsl::Cord
메모리 모델연속 bufferrefcount tree
inline (SSO)15B (libstdc++ x86_64)15B
concatO(n) copyO(log n) tree op
substringO(n) alloc + copyO(log n) zero-copy
copyO(n)O(1) refcount
임의 접근 s[i]O(1)O(log n)
data() 연속 bufferO(1)Flatten() 비용 발생
iterator stableO(1) incrementCharIterator O(log n)
스레드 안전const-share OKconst-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();
}

LoadFromDiskMakeCordFromExternal로 mmap 영역을 복사 없이 흡수한다. RPC 직렬화도 Chunks() 순회 + scatter-gather write로 끝난다.

#코드 리뷰 포인트

1. 크기 기준으로 Cord 적용

조건선택
< 4KBstd::string 또는 string_view
4KB ~ 64KB케이스 바이 케이스
> 64KBCord 강력 검토
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-gather
std::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처럼 사용

// 회피 — 빈번한 mutation
Cord buf;
for (int i = 0; i < 1000000; ++i) {
buf.Append(absl::StrCat(i, "\n"));
}

작은 chunk를 백만 번 추가하면 tree node가 많아지고 cache 영향 큼. std::string + 일정 시점에 Cord로 흡수가 낫다.

Cord 비교를 == 외의 ordering으로

Cordoperator<를 지원하지만 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에서 빠른 숫자 변환을 본다.

#관련 항목

Abseil Code Review · 74 of 79

  1. 1 Abseil Code Review — Google production-grade C++ 라이브러리 분석
  2. 2 Abseil 개요 — Google이 std를 보완한 이유
  3. 3 Abseil 설계 철학 — std 호환과 추가 기능의 균형
  4. 4 Abseil 빌드와 의존성 — Bazel vs CMake
  5. 5 Abseil LTS vs HEAD 릴리스 모델 분석
  6. 6 Abseil Versioning과 ABI 호환성 정책
  7. 7 Abseil 매크로 — ABSL_HAVE_*·ABSL_ATTRIBUTE_*
  8. 8 Abseil ABSL_PREDICT_TRUE/FALSE — branch hint
  9. 9 absl::LogSeverity — 로그 레벨 타입
  10. 10 Abseil type_traits — negation·conjunction·void_t
  11. 11 Abseil Conformance·Policy 분석
  12. 12 Abseil Memory utilities 분석
  13. 13 Abseil raw_logging — heap-free 로깅
  14. 14 Abseil thread_annotations — clang TSA 통합
  15. 15 absl::Status — exception-free error handling
  16. 16 absl::StatusOr<T> — 값 또는 에러
  17. 17 absl status_macros — ASSIGN_OR_RETURN·RETURN_IF_ERROR
  18. 18 absl::Status payload — 구조화된 에러 컨텍스트
  19. 19 absl::Status ↔ exception 변환 패턴
  20. 20 absl::string_view — non-owning 문자열 참조
  21. 21 absl::string_view 함정 — dangling·c_str·임시 객체
  22. 22 absl::StrCat — 가변 인자 문자열 연결과 AlphaNum
  23. 23 absl::StrSplit — Delimiter·Predicate·컨테이너 변환
  24. 24 absl::StrJoin — 컨테이너 결합과 Formatter
  25. 25 absl::StrFormat — type-safe printf·FormatSpec
  26. 26 Abseil ASCII 함수 — locale-free 분류·대소문자 변환
  27. 27 Abseil Escape — CEscape·HexEscape·Base64
  28. 28 absl::flat_hash_map — Swiss Table 기반 hash map
  29. 29 absl::flat_hash_set — set 버전 Swiss Table
  30. 30 absl::node_hash_map — stable pointer가 필요할 때
  31. 31 absl::btree_map — sorted·cache-friendly B-tree
  32. 32 absl::FixedArray — 런타임 크기 stack 배열
  33. 33 absl::InlinedVector — small buffer optimization
  34. 34 Abseil Swiss Table internals — control byte·SIMD probing
  35. 35 absl::Mutex — reader-writer·fairness·deadlock 검출
  36. 36 absl::Mutex Conditional Critical Section — Await로 cv 없애기
  37. 37 absl::Notification — once-only signal
  38. 38 absl::BlockingCounter·Barrier — 다중 thread 조율
  39. 39 absl::Mutex annotations — clang thread-safety로 race를 컴파일 타임에
  40. 40 absl::Time·Duration 분석 — 단단한 type
  41. 41 absl::Time Format·Parse
  42. 42 absl::CivilTime 분석
  43. 43 absl::time_zone 분석
  44. 44 absl::Time mocking — 테스트 친화 시간
  45. 45 absl::BitGen — 모던 난수 생성기
  46. 46 Abseil Random Distributions — Uniform·Exponential
  47. 47 Abseil Mocking Random — 테스트 결정성
  48. 48 Abseil Random Seeding·Entropy
  49. 49 absl::int128·uint128 분석
  50. 50 absl::bits — popcount·countl_zero
  51. 51 absl::optional vs std::optional
  52. 52 absl::variant 분석
  53. 53 absl::span 분석
  54. 54 absl::any 분석
  55. 55 absl::compare — three-way 비교
  56. 56 Abseil utility — apply·in_place
  57. 57 Abseil AbslHashValue 분석
  58. 58 Abseil HashState chaining
  59. 59 Abseil Custom hashable 구현
  60. 60 Abseil LOG·VLOG·CHECK 분석
  61. 61 Abseil LogSink 분석
  62. 62 Abseil LogEntry·structured logging
  63. 63 Abseil Stack trace·failure_signal_handler
  64. 64 ABSL_FLAG 정의 분석
  65. 65 Abseil ParseCommandLine 동작
  66. 66 Abseil Flag introspection·validation
  67. 67 Google 스타일의 Abseil 사용 패턴
  68. 68 Abseil 자주 보는 anti-pattern
  69. 69 std → absl 마이그레이션 전략
  70. 70 absl::Cleanup — 함수 종료 시 실행 보장
  71. 71 Abseil algorithm container 확장 — c_sort·c_find_if·c_count_if
  72. 72 absl::function_ref와 any_invocable — 함수 객체 전달의 두 축
  73. 73 absl::bind_front와 Overload — 함수 객체 보조 도구
  74. 74 absl::Cord — 분산 시스템용 대용량 문자열
  75. 75 absl::from_chars·SimpleAtoi — 빠른 숫자 변환
  76. 76 absl::Cord vs std::string — 선택 기준과 메모리 프로파일
  77. 77 absl::GetStackTrace와 Symbolize — crash 시 readable stack
  78. 78 absl::ComputeCrc32c — 하드웨어 가속 체크섬
  79. 79 absl::PeriodicSampler — 적응형 샘플링·jitter 회피