← 문제로

10. std::string 의 작은 문자열 최적화(SSO)

난이도 중
내 리뷰 · C++
해설 · C++

해설 — std::string 의 작은 문자열 최적화(SSO)

난이도: 중

요약

짧은 문자열은 힙을 쓰지 않고 string 객체 내부의 작은 버퍼에 직접 저장된다 (SSO). 긴 문자열만 힙에 버퍼를 잡는다. 이동(move)은 긴 문자열이면 힙 포인터만 넘겨받아 원본의 데이터 주소가 그대로 대상으로 옮겨 가지만, 짧은 문자열이면 내부 버퍼 내용을 복사해야 하므로 주소가 보존되지 않는다.

실제 출력 (libstdc++, x86-64)

sizeof(std::string) = 32
s1 cap=15 inObject=1
s2 cap=100 inObject=0
s3.data()==old s2.data()? 1
s2 empty after move? 1

핵심 개념

  • libstdc++ 의 std::string 은 32바이트: { char* ptr; size_t size; union { size_t cap; char buf[16]; } }. 즉 15글자 + 널종단까지는 내부 buf 에 담는다 (capacity()==15). 그래서 s1inObject=1(데이터가 객체 내부).
  • 16글자 이상이면 힙에 할당하고 ptr 이 힙을 가리킨다 → s2.inObject=0, capacity() 는 요청·성장 정책에 따른 값(여기선 100).
  • 경계값(15)·sizeof(32)는 구현마다 다르다. libc++ 는 다른 레이아웃/경계를 쓴다(보통 22글자). MSVC 도 다르다. "SSO 가 있다"는 사실은 공통, 수치는 구현 정의.

단계별 내부 동작 (이동)

  1. before = s2.data() — s2 는 긴 문자열이라 힙 버퍼 주소.
  2. s3 = std::move(s2) — 긴 문자열의 이동은 포인터·size·capacity 멤버만 복사하고 s2 를 빈 상태로 만든다. 힙 버퍼는 재할당 없이 그대로 s3 소유. → s3.data() == before참(1).
  3. 이동된 s2 는 "유효하지만 미지정 상태". libstdc++ 는 빈 문자열로 둔다 → s2.empty()==1. (표준이 "비어 있음"을 강제하진 않으니 의존 금지.)
  4. 만약 짧은 문자열을 이동했다면? 데이터가 내부 buf 에 있으므로 포인터만 넘길 수 없고 버퍼 내용을 바이트 복사한다. 즉 SSO 영역의 move 는 사실상 copy 비용이며, 대상의 data() 주소는 원본과 다르다.

흔한 오해·함정

  • "move 는 항상 O(1)·무할당" — 틀림. SSO 범위의 작은 문자열 move 는 내부 버퍼 복사라 길이에 비례하는 작은 비용이 든다(여전히 힙 할당은 없음).
  • "data()/c_str() 포인터는 안정적" — SSO 문자열은 string 객체와 함께 움직이므로, string 을 이동·재할당·벡터 성장시키면 내부 버퍼 주소가 바뀐다. SSO 포인터를 오래 들고 있으면 위험.
  • "짧은 문자열이라 capacity 0" — 아니다. SSO 버퍼만큼(예:15)은 이미 capacity.
  • reserve(작은 값)로 힙 강제 안 됨 — SSO 임계 이하면 여전히 내부 버퍼.

면접 포인트

  • SSO 의 목적: 로그 키·플레이어 닉네임·짧은 패킷 필드처럼 작은 문자열이 압도적 으로 많은 서버 워크로드에서 힙 할당·해제와 캐시 미스를 없앤다.
  • sizeof(string)=32 의 트레이드오프: 내부 버퍼를 위해 객체가 커지지만, 대부분의 짧은 문자열에서 할당 한 번을 통째로 절약.
  • move 의 비용이 "포인터 스왑"이라는 통념이 SSO 타입(string/일부 small-buffer 컨테이너)에선 깨진다는 점을 설명할 수 있는지.