C++ Small String Optimization (SSO) | string 성능 최적화 원리
이 글의 핵심
C++ Small String Optimization (SSO)에 대한 실전 가이드입니다. string 성능 최적화 원리 등을 예제와 함께 상세히 설명합니다.
들어가며: “짧은 문자열이 긴 문자열보다 훨씬 빠른 이유는?"
"string이 힙 할당을 안 하는 경우가 있어요”
C++의 std::string은 짧은 문자열을 힙이 아닌 객체 내부에 저장하는 Small String Optimization(SSO)를 사용합니다.
// 짧은 문자열 (SSO)
std::string short_str = "Hello"; // 힙 할당 없음!
// 긴 문자열
std::string long_str = "This is a very long string that exceeds SSO limit"; // 힙 할당
이 글에서 다루는 것:
- SSO란?
- string 내부 구조
- 성능 측정
- 실전 활용
목차
1. SSO란?
Small String Optimization
SSO는 짧은 문자열을 string 객체 내부 버퍼에 저장하는 최적화입니다.
// 개념적 구조
class string {
union {
// 짧은 문자열: 내부 버퍼 사용
struct {
char buffer[16]; // 15자 + null
uint8_t size;
} short_string;
// 긴 문자열: 힙 할당
struct {
char* data;
size_t size;
size_t capacity;
} long_string;
};
};
SSO 임계값
| 컴파일러 | 플랫폼 | SSO 크기 |
|---|---|---|
| GCC | x64 | 15자 |
| Clang | x64 | 22자 |
| MSVC | x64 | 15자 |
| GCC | x86 | 10자 |
// GCC/MSVC: 15자까지 SSO
std::string s1 = "123456789012345"; // 15자 → SSO
std::string s2 = "1234567890123456"; // 16자 → 힙 할당
2. string 내부 구조
짧은 문자열 (SSO)
// 짧은 문자열
std::string str = "Hello";
// 메모리 레이아웃 (GCC, x64)
// [H][e][l][l][o][\0][...][5]
// ↑ 내부 버퍼 (16바이트) ↑ 크기
긴 문자열 (힙 할당)
// 긴 문자열
std::string str = "This is a very long string";
// 메모리 레이아웃 (GCC, x64)
// [포인터(8)][크기(8)][용량(8)]
// ↓
// [힙 메모리: "This is a very long string\0"]
3. SSO 확인 방법
방법 1: 주소 비교
#include <iostream>
#include <string>
void checkSSO(const std::string& str) {
const void* strAddr = &str;
const void* dataAddr = str.data();
std::cout << "문자열: \"" << str << "\"\n";
std::cout << "길이: " << str.size() << "\n";
std::cout << "string 주소: " << strAddr << "\n";
std::cout << "data() 주소: " << dataAddr << "\n";
if (strAddr == dataAddr ||
(dataAddr >= strAddr &&
dataAddr < (const char*)strAddr + sizeof(std::string))) {
std::cout << "→ SSO (내부 버퍼)\n\n";
} else {
std::cout << "→ 힙 할당\n\n";
}
}
int main() {
checkSSO("Hi"); // SSO
checkSSO("Hello World"); // SSO
checkSSO("123456789012345"); // SSO (15자)
checkSSO("1234567890123456"); // 힙 할당 (16자)
checkSSO("This is a very long string that exceeds SSO"); // 힙 할당
}
// 출력 (GCC):
// 문자열: "Hi"
// 길이: 2
// string 주소: 0x7ffc...
// data() 주소: 0x7ffc...
// → SSO (내부 버퍼)
//
// 문자열: "1234567890123456"
// 길이: 16
// string 주소: 0x7ffc...
// data() 주소: 0x55a8... (다른 주소)
// → 힙 할당
방법 2: sizeof 확인
#include <iostream>
#include <string>
int main() {
std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n';
// GCC/Clang: 32바이트
// MSVC: 32바이트 (x64)
}
4. 성능 측정
벤치마크: 생성/소멸
#include <benchmark/benchmark.h>
// 짧은 문자열 (SSO)
static void BM_ShortString(benchmark::State& state) {
for (auto _ : state) {
std::string str = "Hello"; // SSO
benchmark::DoNotOptimize(str);
}
}
BENCHMARK(BM_ShortString);
// 긴 문자열 (힙 할당)
static void BM_LongString(benchmark::State& state) {
for (auto _ : state) {
std::string str = "This is a very long string that exceeds SSO limit";
benchmark::DoNotOptimize(str);
}
}
BENCHMARK(BM_LongString);
결과 (GCC 13, -O3):
BM_ShortString 1 ns (SSO - 힙 할당 없음)
BM_LongString 50 ns (힙 할당 오버헤드)
벤치마크: 복사
// 짧은 문자열 복사 (SSO)
static void BM_CopyShort(benchmark::State& state) {
std::string str = "Hello";
for (auto _ : state) {
std::string copy = str; // 내부 버퍼 복사
benchmark::DoNotOptimize(copy);
}
}
BENCHMARK(BM_CopyShort);
// 긴 문자열 복사 (힙 할당)
static void BM_CopyLong(benchmark::State& state) {
std::string str = "This is a very long string that exceeds SSO limit";
for (auto _ : state) {
std::string copy = str; // 힙 할당 + 메모리 복사
benchmark::DoNotOptimize(copy);
}
}
BENCHMARK(BM_CopyLong);
결과 (GCC 13, -O3):
BM_CopyShort 2 ns (버퍼 복사)
BM_CopyLong 100 ns (힙 할당 + 복사)
5. 실전 활용
활용 1: 짧은 문자열 선호
// ✅ SSO 활용
std::vector<std::string> names;
names.reserve(1000);
for (int i = 0; i < 1000; ++i) {
// 짧은 이름 → SSO
names.emplace_back("User" + std::to_string(i)); // "User123" → SSO
}
// ❌ 긴 문자열 → 힙 할당
for (int i = 0; i < 1000; ++i) {
names.emplace_back("This is a very long user name: " + std::to_string(i));
}
활용 2: 문자열 연결 최소화
// ❌ 여러 번 연결 → SSO 초과
std::string buildPath(const std::string& dir, const std::string& file) {
std::string path = dir; // 복사
path += "/"; // SSO 초과 가능
path += file; // 힙 할당
return path;
}
// ✅ 한 번에 생성
std::string buildPath(const std::string& dir, const std::string& file) {
std::string path;
path.reserve(dir.size() + 1 + file.size()); // 미리 공간 확보
path += dir;
path += "/";
path += file;
return path;
}
활용 3: 임시 문자열 회피
// ❌ 임시 문자열 생성
void log(const std::string& msg) {
std::cout << "[LOG] " + msg + "\n"; // 임시 string 생성
}
// ✅ string_view 사용 (C++17)
void log(std::string_view msg) {
std::cout << "[LOG] " << msg << "\n"; // 복사 없음
}
SSO 구현 예시
간단한 SSO 구현
class SmallString {
static constexpr size_t SSO_SIZE = 15;
union {
// 짧은 문자열
struct {
char buffer[SSO_SIZE + 1]; // +1 for null
uint8_t size;
} sso;
// 긴 문자열
struct {
char* data;
size_t size;
size_t capacity;
} heap;
};
bool isSSO() const {
return sso.size <= SSO_SIZE;
}
public:
SmallString(const char* str) {
size_t len = std::strlen(str);
if (len <= SSO_SIZE) {
// SSO 사용
std::memcpy(sso.buffer, str, len + 1);
sso.size = static_cast<uint8_t>(len);
std::cout << "SSO 사용 (길이: " << len << ")\n";
} else {
// 힙 할당
heap.size = len;
heap.capacity = len + 1;
heap.data = new char[heap.capacity];
std::memcpy(heap.data, str, len + 1);
std::cout << "힙 할당 (길이: " << len << ")\n";
}
}
~SmallString() {
if (!isSSO()) {
delete[] heap.data;
}
}
const char* c_str() const {
return isSSO() ? sso.buffer : heap.data;
}
size_t size() const {
return isSSO() ? sso.size : heap.size;
}
};
int main() {
SmallString s1("Hello"); // SSO 사용 (길이: 5)
SmallString s2("123456789012345"); // SSO 사용 (길이: 15)
SmallString s3("1234567890123456");// 힙 할당 (길이: 16)
std::cout << s1.c_str() << '\n';
std::cout << s2.c_str() << '\n';
std::cout << s3.c_str() << '\n';
}
실전 예시
예시 1: 로그 메시지
// ✅ SSO 활용: 짧은 로그 레벨
enum class LogLevel {
DEBUG, INFO, WARN, ERROR
};
std::string getLevelString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG"; // SSO
case LogLevel::INFO: return "INFO"; // SSO
case LogLevel::WARN: return "WARN"; // SSO
case LogLevel::ERROR: return "ERROR"; // SSO
}
}
void log(LogLevel level, const std::string& msg) {
std::string levelStr = getLevelString(level); // SSO → 힙 할당 없음
std::cout << "[" << levelStr << "] " << msg << '\n';
}
int main() {
for (int i = 0; i < 1000000; ++i) {
log(LogLevel::INFO, "메시지"); // 빠름!
}
}
예시 2: 키-값 저장
// ✅ SSO 활용: 짧은 키
std::map<std::string, int> scores;
// 짧은 키 → SSO
scores["Alice"] = 100; // SSO
scores["Bob"] = 90; // SSO
scores["Charlie"] = 85; // SSO
// 긴 키 → 힙 할당
scores["VeryLongUserNameThatExceedsSSO"] = 80; // 힙 할당
예시 3: 문자열 파싱
// ✅ SSO 활용: 토큰 파싱
std::vector<std::string> tokenize(const std::string& str, char delim) {
std::vector<std::string> tokens;
std::stringstream ss(str);
std::string token;
while (std::getline(ss, token, delim)) {
tokens.push_back(token); // 짧은 토큰 → SSO
}
return tokens;
}
int main() {
auto tokens = tokenize("a,b,c,d,e,f,g", ',');
// 모든 토큰이 SSO → 힙 할당 없음!
for (const auto& token : tokens) {
std::cout << token << '\n';
}
}
성능 최적화 팁
팁 1: 짧은 문자열 유지
// ✅ 짧은 문자열
std::string status = "OK"; // SSO
std::string code = "200"; // SSO
std::string method = "GET"; // SSO
// ❌ 불필요하게 긴 문자열
std::string status = "Status: OK"; // 힙 할당 가능
팁 2: reserve() 사용
// ✅ reserve()로 재할당 방지
std::string buildUrl(const std::string& host, const std::string& path) {
std::string url;
url.reserve(host.size() + path.size() + 10); // "https://" + "/"
url = "https://";
url += host;
url += "/";
url += path;
return url;
}
팁 3: string_view 사용
// ✅ string_view: 복사 없음
void process(std::string_view str) {
// 문자열 복사 없음
if (str.starts_with("http")) {
std::cout << "URL\n";
}
}
int main() {
std::string url = "https://example.com";
process(url); // 복사 없음
}
정리
SSO 활용 가이드
| 상황 | 권장 |
|---|---|
| 짧은 문자열 (≤15자) | SSO 자동 적용 |
| 긴 문자열 | reserve() 사용 |
| 문자열 연결 | reserve() + += |
| 읽기만 | string_view |
| 임시 문자열 | 회피 |
핵심 규칙
- 짧은 문자열 선호 (SSO 활용)
- reserve()로 재할당 방지
- string_view로 복사 회피
- 문자열 연결 최소화
체크리스트
- 문자열이 15자 이하인가?
- reserve()를 사용하는가?
- 불필요한 문자열 복사가 있는가?
- string_view를 사용할 수 있는가?
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ string 기초 | 완벽 가이드
- C++ string vs string_view | 비교
- C++ 성능 최적화 | 병목 찾기
- C++ 메모리 최적화 | 할당 최소화
이 글에서 다루는 키워드 (관련 검색어)
Small String Optimization, SSO, string 성능, 힙 할당, string 내부 구조 등으로 검색하시면 이 글이 도움이 됩니다.
자주 하는 실수
실수 1: 불필요한 문자열 연결
// ❌ 실수: 여러 번 연결 → SSO 초과
std::string buildMessage() {
std::string msg = "Error: "; // SSO
msg += "File not found: "; // SSO 초과
msg += "/very/long/path/to/file.txt"; // 힙 재할당
return msg;
}
// ✅ 한 번에 생성
std::string buildMessage() {
return "Error: File not found: /very/long/path/to/file.txt";
// 또는 reserve() 사용
}
실수 2: 임시 문자열 생성
// ❌ 실수: 임시 문자열
void log(const std::string& msg) {
std::cout << "[LOG] " + msg + "\n"; // 임시 string 2개 생성
}
// ✅ string_view 사용
void log(std::string_view msg) {
std::cout << "[LOG] " << msg << "\n"; // 복사 없음
}
실수 3: 긴 문자열 리터럴
// ❌ 실수: 긴 리터럴을 string으로
const std::string ERROR_MSG = "This is a very long error message...";
// 프로그램 시작 시 힙 할당
// ✅ string_view 또는 const char* 사용
constexpr std::string_view ERROR_MSG = "This is a very long error message...";
// 또는
constexpr const char* ERROR_MSG = "This is a very long error message...";
실무 트러블슈팅
문제: 예상보다 많은 힙 할당
증상:
# Valgrind로 힙 할당 확인
$ valgrind --tool=massif ./myapp
# 예상보다 많은 malloc 호출
진단:
// 문자열 길이 확인
std::string str = "Hello";
std::cout << "길이: " << str.size() << '\n';
std::cout << "용량: " << str.capacity() << '\n';
// SSO 확인
const void* strAddr = &str;
const void* dataAddr = str.data();
if (strAddr == dataAddr ||
(dataAddr >= strAddr && dataAddr < (const char*)strAddr + sizeof(std::string))) {
std::cout << "SSO 사용\n";
} else {
std::cout << "힙 할당\n";
}
해결:
// 1. 문자열 짧게 유지
// 2. reserve() 사용
// 3. string_view 활용
문제: 문자열 복사 성능 저하
증상: 문자열 복사가 느림
원인: SSO 초과로 힙 할당 발생
해결:
// ✅ 짧은 키 사용
std::map<std::string, int> cache;
cache["usr"] = 1; // SSO
cache["cfg"] = 2; // SSO
// ❌ 긴 키
cache["very_long_configuration_key_name"] = 3; // 힙 할당
컴파일러별 SSO 크기
상세 비교
| 컴파일러 | 플랫폼 | SSO 크기 | sizeof(string) | 비고 |
|---|---|---|---|---|
| GCC 11+ | x64 | 15자 | 32바이트 | 표준 |
| Clang 14+ | x64 | 22자 | 24바이트 | 더 큼 |
| MSVC 2022 | x64 | 15자 | 32바이트 | GCC와 동일 |
| GCC | x86 | 10자 | 24바이트 | 32비트 |
| libc++ | x64 | 22자 | 24바이트 | Clang 표준 라이브러리 |
// 컴파일러 확인
#ifdef __GNUC__
std::cout << "GCC " << __GNUC__ << '\n';
#elif defined(_MSC_VER)
std::cout << "MSVC " << _MSC_VER << '\n';
#elif defined(__clang__)
std::cout << "Clang " << __clang_major__ << '\n';
#endif
std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n';
베스트 프랙티스
1. 문자열 길이 가이드
// ✅ SSO 활용
std::string status = "OK"; // 2자 - SSO
std::string method = "GET"; // 3자 - SSO
std::string code = "200"; // 3자 - SSO
std::string type = "application"; // 11자 - SSO
// ⚠️ SSO 경계
std::string path = "/api/users/123"; // 14자 - SSO (GCC)
std::string uuid = "550e8400-e29b"; // 16자 - 힙 할당 (GCC)
2. 문자열 빌더 패턴
// ✅ 효율적인 문자열 빌더
class StringBuilder {
std::string buffer_;
public:
StringBuilder& append(std::string_view str) {
buffer_ += str;
return *this;
}
StringBuilder& reserve(size_t size) {
buffer_.reserve(size);
return *this;
}
std::string build() {
return std::move(buffer_);
}
};
// 사용
auto str = StringBuilder()
.reserve(100) // 미리 공간 확보
.append("Hello")
.append(" ")
.append("World")
.build();
3. 코드 리뷰 체크포인트
// 🔍 리뷰 시 확인사항
// 1. 문자열 연결
std::string msg = a + b + c; // ⚠️ 임시 객체 2개
// 2. 반복문 내 문자열 생성
for (int i = 0; i < 1000; ++i) {
std::string temp = "prefix_" + std::to_string(i); // ⚠️ 힙 할당
}
// 3. 함수 파라미터
void process(std::string s); // ⚠️ 복사 발생
void process(std::string_view s); // ✅ 복사 없음
실무 시나리오
시나리오 1: HTTP 헤더 파싱
// ✅ 실무 예시: HTTP 헤더
class HttpHeaders {
std::map<std::string, std::string> headers_;
public:
void parse(const std::string& headerLine) {
auto pos = headerLine.find(':');
if (pos != std::string::npos) {
// 짧은 헤더 이름 → SSO
std::string name = headerLine.substr(0, pos); // "Host", "Accept" 등
std::string value = headerLine.substr(pos + 2);
headers_[name] = value;
}
}
};
// 대부분의 HTTP 헤더 이름은 15자 이하
// Host, Accept, Content-Type, User-Agent 등
시나리오 2: 로그 시스템
// ✅ 실무 예시: 로그 레벨
enum class LogLevel {
DEBUG, INFO, WARN, ERROR, FATAL
};
std::string_view getLevelString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG"; // 5자 - SSO
case LogLevel::INFO: return "INFO"; // 4자 - SSO
case LogLevel::WARN: return "WARN"; // 4자 - SSO
case LogLevel::ERROR: return "ERROR"; // 5자 - SSO
case LogLevel::FATAL: return "FATAL"; // 5자 - SSO
}
}
// 로그 메시지
void log(LogLevel level, std::string_view msg) {
// 짧은 레벨 문자열 → SSO
std::string levelStr(getLevelString(level));
std::cout << "[" << levelStr << "] " << msg << '\n';
}
시나리오 3: 설정 키
// ✅ 실무 예시: 설정 키
class Config {
std::map<std::string, std::string> values_;
public:
// 짧은 키 사용 → SSO
void set(std::string_view key, std::string_view value) {
values_[std::string(key)] = value;
}
std::optional<std::string> get(std::string_view key) const {
auto it = values_.find(std::string(key));
if (it != values_.end()) {
return it->second;
}
return std::nullopt;
}
};
// 사용 - 짧은 키 선호
config.set("port", "8080"); // 4자 - SSO
config.set("host", "localhost"); // 4자 - SSO
config.set("debug", "true"); // 5자 - SSO
성능 프로파일링
힙 할당 추적
// 커스텀 allocator로 추적
template <typename T>
class TrackingAllocator {
public:
using value_type = T;
T* allocate(size_t n) {
std::cout << "할당: " << n * sizeof(T) << " 바이트\n";
return static_cast<T*>(::operator new(n * sizeof(T)));
}
void deallocate(T* p, size_t n) {
std::cout << "해제: " << n * sizeof(T) << " 바이트\n";
::operator delete(p);
}
};
// 사용
using TrackedString = std::basic_string<char, std::char_traits<char>,
TrackingAllocator<char>>;
TrackedString s1 = "Hello"; // SSO - 할당 없음
TrackedString s2 = "This is a very long string"; // 힙 할당 출력
마치며
Small String Optimization(SSO)는 짧은 문자열의 힙 할당을 제거하는 강력한 최적화입니다.
핵심 원칙:
- 짧은 문자열 선호 (≤15자)
- reserve()로 재할당 방지
- string_view로 복사 회피
실무 팁:
- HTTP 헤더, 로그 레벨, 설정 키는 짧게
- 문자열 연결 시 reserve() 사용
- 프로파일러로 힙 할당 확인
짧은 문자열을 사용하면 SSO가 자동으로 적용되어 성능이 크게 향상됩니다.
다음 단계: SSO를 이해했다면, C++ string_view 가이드에서 더 깊이 배워보세요.
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |