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). 그래서s1은inObject=1(데이터가 객체 내부). - 16글자 이상이면 힙에 할당하고
ptr이 힙을 가리킨다 →s2.inObject=0,capacity()는 요청·성장 정책에 따른 값(여기선 100). - 경계값(15)·sizeof(32)는 구현마다 다르다. libc++ 는 다른 레이아웃/경계를 쓴다(보통 22글자). MSVC 도 다르다. "SSO 가 있다"는 사실은 공통, 수치는 구현 정의.
단계별 내부 동작 (이동)
before = s2.data()— s2 는 긴 문자열이라 힙 버퍼 주소.s3 = std::move(s2)— 긴 문자열의 이동은 포인터·size·capacity 멤버만 복사하고 s2 를 빈 상태로 만든다. 힙 버퍼는 재할당 없이 그대로 s3 소유. →s3.data() == before가 참(1).- 이동된 s2 는 "유효하지만 미지정 상태". libstdc++ 는 빈 문자열로 둔다
→
s2.empty()==1. (표준이 "비어 있음"을 강제하진 않으니 의존 금지.) - 만약 짧은 문자열을 이동했다면? 데이터가 내부
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 컨테이너)에선 깨진다는 점을 설명할 수 있는지.