folly::FBString 분석 — SSO + COW 구현
#한 줄 요약
folly::fbstring은 23-byte SSO와 medium 영역 eager copy, large 영역 copy-on-write를 결합한 3-tier 문자열이다. std::string보다 단편화가 적고 jemalloc 친화적이며 ABI는 호환된다.
#동기
std::string은 구현체마다 다르다. libstdc++ 5 이전은 COW, 이후는 SSO 15-byte, libc++/MSVC는 SSO 22-byte. 데이터 센터 규모에서 이 차이는 무시할 수 없다.
Meta는 두 가지 요구가 있었다. 첫째, 짧은 문자열(URL path 조각, key, log tag)의 힙 할당을 없애야 한다. 둘째, 큰 페이로드(HTML, JSON blob)는 함수 인자로 자주 복사되는데 매번 복사하면 throughput이 떨어진다. SSO와 COW를 한 타입에 합쳐 둘 다 해결한다.
folly::fbstring small = "hello"; // SSO: 힙 할당 0회folly::fbstring large = ReadFile("doc"); // 큰 데이터folly::fbstring copy = large; // COW: refcount만 증가copy.push_back('!'); // 이때 분리(unshare)핵심은 세 영역을 한 24-byte 객체에 우겨넣는다는 점이다.
#API & 사용법
fbstring은 std::string의 거의 모든 멤버를 제공한다. c_str(), data(), size(), operator[], append, replace, iterator 모두 동일하다. 추가로 다음이 있다.
#include <folly/FBString.h>
folly::fbstring s = "abc";s.reserve(1000); // capacity 확장s += "def";
// std::string과 zero-copy 변환std::string std_s = s.toStdString(); // 새 복사folly::fbstring back(std_s.data(), std_s.size());
// StringPiece와 호환folly::StringPiece sp = s;-DFOLLY_USE_FBSTRING_FOR_STD_STRING을 정의하면 std::string을 매크로로 fbstring으로 치환할 수 있다. fbcode 내부에서 쓰는 트릭이지만 외부에서는 ABI 충돌 위험이 있어 권장하지 않는다.
#내부 구현
fbstring의 24-byte 객체는 마지막 byte로 카테고리를 식별한다.
// folly/FBString.h 의 약식struct MediumLarge { char* data_; // 8 bytes size_t size_; // 8 bytes size_t capacity_; // 8 bytes — 상위 2-bit이 카테고리};
struct Small { char data_[23]; // 23 bytes uint8_t lastChar_; // 상위 2-bit + (23 - size)};capacity_의 상위 2-bit은 little-endian 머신에서 Small.lastChar_의 상위 2-bit과 같은 byte에 위치한다. 이 2-bit이 카테고리다.
| Category | 2-bit | 조건 |
|---|---|---|
| isSmall | 00 | size ≤ 23 |
| isMedium | 10 | 23 < size ≤ 254 |
| isLarge | 11 | size > 254, refcount 사용 |
Small 모드에서 size()는 23 - lastChar_(상위 2-bit 마스킹 후)로 계산한다. 빈 문자열이면 lastChar_ = 23이고 data_[0] = '\0'이라 c_str()이 그대로 동작한다.
#Medium — eager copy
23 < size ≤ 254 구간은 힙에 할당하되 복사 시 즉시 deep copy 한다. refcount overhead 없이 단순하다. malloc은 goodMallocSize()로 jemalloc class에 맞춰 올림한다.
// 약식static size_t goodMallocSize(size_t n) { if (n <= 64) return ((n + 7) / 8) * 8; // 그 외 jemalloc size class}#Large — Copy-on-Write
255 byte 이상이면 다음 레이아웃이다.
[ refcount (8B) | char[] ... | '\0' ] ↑ heap pointer 의 -8 위치data_는 char 시작점을 가리키고, data_ - sizeof(size_t) 위치에 atomic refcount가 있다. 복사 생성자는 __atomic_fetch_add(refcount, 1, RELAXED)만 호출하고 반환한다.
수정 시 분리(unshare):
// 약식char* mutableData() { if (category() == isLarge && refCount() > 1) { // deep copy 후 자기만의 buffer 보유 unshare(); } return data_;}이 unshare는 operator[]의 non-const 오버로드, data() non-const, iterator 시작에서 호출된다. const 접근은 분리하지 않는다.
#Copy-on-Write 동작 그림
Large 영역에서 vs1 = vs2가 일어나면 ptr만 복사 + refcount++. 둘 중 하나가 수정 시에 비로소 새 버퍼를 떼낸다.
이 lazy split이 큰 페이로드의 함수 인자 전달을 사실상 무료로 만든다. 다만 멀티스레드에서 atomic refcount의 contention 비용이 있어 large가 read-heavy일 때 가장 잘 동작한다.
#Why 254, not 255?
254 + 1('\0')이 jemalloc 256 class와 정확히 맞는다. 한 byte 더 쓰면 다음 class(384)로 올라가 단편이 생긴다. 이런 byte 단위 튜닝이 Folly다.
#std::string 비교
| 항목 | std::string (libstdc++ 5+) | folly::fbstring |
|---|---|---|
| SSO size | 15 byte | 23 byte |
| COW | 없음 | size > 254에서 사용 |
| sizeof | 32 byte | 24 byte |
| jemalloc 친화 | 일반적 | goodMallocSize() 사용 |
| ABI | 표준 | Meta 사양 |
| const data() race | 안전 | 안전 (atomic refcount) |
23-byte SSO는 24-byte 객체에 마지막 1-byte로 size까지 인코딩한 결과다. libstdc++/libc++가 15 또는 22-byte에 멈춘 이유는 별도의 size 필드를 두기 때문이다.
abseil은 absl::Cord로 다른 방향을 택했다. Cord는 작은 조각의 트리로 큰 문자열을 표현해 substring·append를 O(log n)으로 만든다. fbstring은 contiguous를 유지한다(C API 호환을 위해).
#코드 리뷰 포인트
// Bad — std::string으로 받았다가 fbstring으로 복사void Process(const std::string& s) { folly::fbstring fs(s); // deep copy // ...}
// Good — StringPiece로 받으면 양쪽 다 view 만 잡는다void Process(folly::StringPiece s) { // ...}API boundary에서 타입을 어떻게 받느냐가 가장 큰 비용이다. 함수가 소유권을 가지지 않는다면 항상 StringPiece(또는 std::string_view)로 받는다.
// 위험 — large fbstring을 비-const iterator로 순회for (auto& c : large_fb_str) { // unshare 발생! Sanitize(c);}
// 안전 — 의도가 read-only면 constfor (const auto& c : large_fb_str) { Inspect(c);}auto&는 non-const reference라 large mode에서 분리를 강제한다. 의도와 다르면 silent overhead가 생긴다.
#안티패턴
fbstring을 STL 컨테이너 key로 ABI 경계에서 노출: 다른 라이브러리와 컨테이너 타입이 갈리면 변환 비용이 매번 든다. boundary는std::string또는StringPiece로 통일.reserve(0)으로 SSO 강제 시도: 일단 medium/large로 올라간 buffer는shrink_to_fit()을 호출해도 SSO로 다시 내려오지 않을 수 있다. 짧다고 알면 처음부터 짧게.- multi-thread에서 mutable iterator 공유: large mode COW는 atomic refcount지만 동일 객체의 mutable 작업은 여전히 race다. 공유하려면
const로 read, 수정 전에 deep copy.
#정리
fbstring은 23-byte SSO, eager-copy medium, COW large의 3-tier 설계.- 24-byte 객체에 상위 2-bit 카테고리 인코딩으로 별도 size 필드 없이 SSO 23-byte 달성.
- Large mode COW는 atomic refcount로 multi-thread const 접근 안전.
- jemalloc size class에 맞춘
goodMallocSize()로 단편 최소화. - API boundary는
StringPiece로 받아 타입 변환 비용 회피.
#다음 편
다음은 Folly가 표준 <format> 대신 {fmt} 라이브러리를 채택한 이유와 통합 방식을 본다.
#관련 항목
- Part 5-03: StringPiece — fbstring의 view 짝
- Part 5-04: Join / split utilities — StringPiece 기반 split
- Part 1-02: Folly vs Abseil 철학 — Cord vs fbstring 설계 차이
- 원문 — folly/FBString.h
Folly Code Review · 24 of 89
- 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
- 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
- 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
- 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
- 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
- 6 Folly production validation 문화 — peta-scale에서 단련된 코드
- 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
- 8 folly::Promise·makeFuture — Future를 만드는 두 길
- 9 folly::SemiFuture vs Future — executor binding의 명시화
- 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
- 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
- 12 folly::Future retry·window·via — 제어 흐름 조합자
- 13 folly::fibers 분석 — M:N stackful coroutine
- 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
- 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
- 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
- 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
- 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
- 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
- 20 folly::IOBufQueue — chain의 push/pull 추상화
- 21 folly::io::Cursor·RWCursor — chain 위의 stream
- 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
- 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
- 24 folly::FBString 분석 — SSO + COW 구현
- 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
- 26 folly::StringPiece — string_view 호환 분석
- 27 folly Join·Split utilities — 문자열 분해와 결합
- 28 folly::to·tryTo — text↔num 변환 분석
- 29 folly Conv Customization — 사용자 타입 지원
- 30 folly Conv 성능 비교 — sprintf·stringstream 대비
- 31 folly::F14ValueMap vs std::unordered_map
- 32 folly::F14NodeMap — stable pointer가 필요할 때
- 33 folly::F14VectorMap — cache-friendly iteration
- 34 folly::F14FastMap — auto-select 동작
- 35 folly F14 internals — SIMD probing 메커니즘
- 36 folly::small_vector — inline storage 분석
- 37 folly::FixedString — compile-time string
- 38 folly::AtomicHashMap — lock-free read 분석
- 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
- 40 folly::EvictingCacheMap — LRU 구현 분석
- 41 folly::Synchronized — lock wrapper 패턴
- 42 folly::SharedMutex 분석
- 43 folly::Baton — one-shot wait 동기화
- 44 folly::RWSpinLock 분석
- 45 folly::PicoSpinLock — 1-byte spinlock
- 46 folly::ProducerConsumerQueue — SPSC 큐 분석
- 47 folly::MPMCQueue — multi-producer multi-consumer
- 48 folly::UnboundedQueue — 동적 크기 lock-free
- 49 folly::fibers::Channel — Go-like channel
- 50 folly::dynamic — JSON-like dynamic type 분석
- 51 folly JSON conversion — toJson·parseJson
- 52 folly dynamic ↔ struct — manual marshaling
- 53 folly dynamic Visitor pattern — type별 분기
- 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
- 55 folly::SingletonVault 분석 — 등록·소멸·의존성
- 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
- 57 folly::ExceptionWrapper — type-erased exception holder
- 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
- 59 folly::Optional vs std::optional
- 60 folly::Function vs std::function
- 61 folly::Lazy — 지연 초기화 wrapper
- 62 folly Meta 스타일 code review 패턴
- 63 folly anti-patterns — 잘못 쓰면 std보다 느림
- 64 folly vs std 선택 기준 분석
- 65 folly::coro 개요 — production C++20 코루틴 어댑터
- 66 folly::coro::Task — lazy single-shot 코루틴
- 67 folly::coro::AsyncGenerator — 비동기 스트림
- 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
- 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
- 70 folly::Expected — 결과 또는 오류
- 71 folly::Try — Future 결과 wrapper
- 72 folly::Try vs Expected 선택 기준
- 73 folly::Range — 일반 iterator pair
- 74 folly::Uri — URL 파서
- 75 folly Fingerprint64·128 — 분산 hash
- 76 folly SpookyHashV2 — fast non-crypto hash
- 77 folly::Init — main() 부트스트랩
- 78 folly::Indestructible — global lifetime 패턴
- 79 folly::MicroLock — 1-byte 락
- 80 folly::MicroSpinLock — 가장 좁은 spin lock
- 81 folly::format — legacy formatter 분석
- 82 folly::demangle — typeid 디망글링
- 83 folly::DynamicConverter — dynamic ↔ struct
- 84 folly::RecordIO — append-only 로그 파일 포맷
- 85 folly::io::Compression — zstd·lz4·snappy wrapper
- 86 folly::AsyncIO — io_uring·Linux AIO
- 87 folly::CancellationToken — 코루틴·Future 취소 전파
- 88 folly::observer — hot config의 atomic refresh
- 89 fbcode 패턴 모음 — folly 사용의 실전
관련 글
fbcode 패턴 모음 — folly 사용의 실전
Meta fbcode 코드 리뷰에서 반복적으로 등장하는 folly 사용 패턴 — overview + 시리즈 마무리.
같은 시리즈에서 이어 읽기
folly::observer — hot config의 atomic refresh
folly::observer — read mostly 값의 atomic refresh, hot config·feature flag·LB weight 같은 패턴의 표준.
같은 시리즈에서 이어 읽기
folly::CancellationToken — 코루틴·Future 취소 전파
CancellationSource/Token의 전파 모델 — coroutine·Future·callback 트리에서 협력적 취소.
같은 시리즈에서 이어 읽기