본문으로 건너뛰기
Folly Code Review · 23/89

folly::FBString 분석 — SSO + COW 구현

· Hawk · 5분 읽기

#한 줄 요약

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 & 사용법

fbstringstd::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로 카테고리를 식별한다.

FBString 24-byte layout

// 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이 카테고리다.

Category2-bit조건
isSmall00size ≤ 23
isMedium1023 < size ≤ 254
isLarge11size > 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++. 둘 중 하나가 수정 시에 비로소 새 버퍼를 떼낸다.

Copy-on-Write split

이 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 size15 byte23 byte
COW없음size > 254에서 사용
sizeof32 byte24 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면 const
for (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} 라이브러리를 채택한 이유와 통합 방식을 본다.

#관련 항목

Folly Code Review · 24 of 89

  1. 1 Folly Code Review — Meta의 production-grade C++ 라이브러리 코드 분석
  2. 2 Folly 개요 — Meta가 production에서 검증한 utility 모음 분석
  3. 3 Folly vs Abseil 철학 비교 — performance-first vs std-compatible
  4. 4 Folly 빌드와 fbcode 환경 — monorepo의 그림자
  5. 5 Folly API stability 정책 — 어떤 보장도 없다는 솔직함
  6. 6 Folly production validation 문화 — peta-scale에서 단련된 코드
  7. 7 folly::Future 분석 — std::future의 한계를 넘는 composable async
  8. 8 folly::Promise·makeFuture — Future를 만드는 두 길
  9. 9 folly::SemiFuture vs Future — executor binding의 명시화
  10. 10 folly::Future thenValue·thenError·thenTry — continuation 체인 분석
  11. 11 folly::collect·collectAll·collectAny — fan-in 패턴 분석
  12. 12 folly::Future retry·window·via — 제어 흐름 조합자
  13. 13 folly::fibers 분석 — M:N stackful coroutine
  14. 14 folly::InlineExecutor — 호출자 thread에서 즉시 실행
  15. 15 folly::CPUThreadPoolExecutor — CPU-bound 작업의 표준 thread pool
  16. 16 folly::IOThreadPoolExecutor — libevent 기반 I/O pool
  17. 17 folly::ManualExecutor — 결정적 테스트를 위한 수동 진행
  18. 18 folly::EventBase 분석 — libevent 이벤트 루프의 핵심
  19. 19 folly::IOBuf 분석 — zero-copy buffer chain의 기본 단위
  20. 20 folly::IOBufQueue — chain의 push/pull 추상화
  21. 21 folly::io::Cursor·RWCursor — chain 위의 stream
  22. 22 folly Zero-copy 패턴 — IOBuf로 ScatterGather I/O 표현
  23. 23 folly::IOBuf shared semantics — clone·unshare·takeOwnership
  24. 24 folly::FBString 분석 — SSO + COW 구현
  25. 25 folly의 fmt::format 통합 — 모던 포맷팅 채택
  26. 26 folly::StringPiece — string_view 호환 분석
  27. 27 folly Join·Split utilities — 문자열 분해와 결합
  28. 28 folly::to·tryTo — text↔num 변환 분석
  29. 29 folly Conv Customization — 사용자 타입 지원
  30. 30 folly Conv 성능 비교 — sprintf·stringstream 대비
  31. 31 folly::F14ValueMap vs std::unordered_map
  32. 32 folly::F14NodeMap — stable pointer가 필요할 때
  33. 33 folly::F14VectorMap — cache-friendly iteration
  34. 34 folly::F14FastMap — auto-select 동작
  35. 35 folly F14 internals — SIMD probing 메커니즘
  36. 36 folly::small_vector — inline storage 분석
  37. 37 folly::FixedString — compile-time string
  38. 38 folly::AtomicHashMap — lock-free read 분석
  39. 39 folly::ConcurrentHashMap — sharded 동시 해시 맵
  40. 40 folly::EvictingCacheMap — LRU 구현 분석
  41. 41 folly::Synchronized — lock wrapper 패턴
  42. 42 folly::SharedMutex 분석
  43. 43 folly::Baton — one-shot wait 동기화
  44. 44 folly::RWSpinLock 분석
  45. 45 folly::PicoSpinLock — 1-byte spinlock
  46. 46 folly::ProducerConsumerQueue — SPSC 큐 분석
  47. 47 folly::MPMCQueue — multi-producer multi-consumer
  48. 48 folly::UnboundedQueue — 동적 크기 lock-free
  49. 49 folly::fibers::Channel — Go-like channel
  50. 50 folly::dynamic — JSON-like dynamic type 분석
  51. 51 folly JSON conversion — toJson·parseJson
  52. 52 folly dynamic ↔ struct — manual marshaling
  53. 53 folly dynamic Visitor pattern — type별 분기
  54. 54 folly::Singleton vs Meyers/static — 왜 Folly의 Singleton인가
  55. 55 folly::SingletonVault 분석 — 등록·소멸·의존성
  56. 56 folly::Singleton try_get·try_get_fast — TLS-cached 접근
  57. 57 folly::ExceptionWrapper — type-erased exception holder
  58. 58 folly::ScopeGuard·SCOPE_EXIT — RAII cleanup
  59. 59 folly::Optional vs std::optional
  60. 60 folly::Function vs std::function
  61. 61 folly::Lazy — 지연 초기화 wrapper
  62. 62 folly Meta 스타일 code review 패턴
  63. 63 folly anti-patterns — 잘못 쓰면 std보다 느림
  64. 64 folly vs std 선택 기준 분석
  65. 65 folly::coro 개요 — production C++20 코루틴 어댑터
  66. 66 folly::coro::Task — lazy single-shot 코루틴
  67. 67 folly::coro::AsyncGenerator — 비동기 스트림
  68. 68 folly coro blockingWait·collectAll — 동기 경계와 fan-in
  69. 69 folly::coro::Baton·Mutex — 코루틴-aware 동기화
  70. 70 folly::Expected — 결과 또는 오류
  71. 71 folly::Try — Future 결과 wrapper
  72. 72 folly::Try vs Expected 선택 기준
  73. 73 folly::Range — 일반 iterator pair
  74. 74 folly::Uri — URL 파서
  75. 75 folly Fingerprint64·128 — 분산 hash
  76. 76 folly SpookyHashV2 — fast non-crypto hash
  77. 77 folly::Init — main() 부트스트랩
  78. 78 folly::Indestructible — global lifetime 패턴
  79. 79 folly::MicroLock — 1-byte 락
  80. 80 folly::MicroSpinLock — 가장 좁은 spin lock
  81. 81 folly::format — legacy formatter 분석
  82. 82 folly::demangle — typeid 디망글링
  83. 83 folly::DynamicConverter — dynamic ↔ struct
  84. 84 folly::RecordIO — append-only 로그 파일 포맷
  85. 85 folly::io::Compression — zstd·lz4·snappy wrapper
  86. 86 folly::AsyncIO — io_uring·Linux AIO
  87. 87 folly::CancellationToken — 코루틴·Future 취소 전파
  88. 88 folly::observer — hot config의 atomic refresh
  89. 89 fbcode 패턴 모음 — folly 사용의 실전