약어의 의미 std :: string


155

에서 최적화 및 코드 스타일에 대한 C ++ 질문 몇 가지 답변의 사본을 최적화의 맥락에서 "SSO"라고 std::string. 그 맥락에서 SSO는 무엇을 의미합니까?

"단일 사인온"이 아닙니다. 아마도 "공유 문자열 최적화"입니까?


57
그것은 "2 + 2는 무엇인가"가 "200/50의 결과는 무엇인가"의 복제물과 같은 방식으로 만 복제 된 것입니다. 대답은 같습니다. 질문은 완전히 다릅니다. "중복으로 닫기"는 여러 사람이 같은 * 질문을 할 때 사용하기위한 것입니다. 한 사람이 "방법을 요청하면 std::string구현", 그리고 다른 하나는 "SSO 평균 무엇"묻는다, 당신은 같은 질문 수를 고려하는 것이 절대적으로 미친해야
jalf

1
@ jalf :이 질문의 범위를 정확하게 포함하는 기존 Q + A가있는 경우 중복으로 간주합니다 (OP가 OP를 직접 검색해야한다고 말하지는 않습니다. 단지 여기에있는 답변은 이미 다루었).
올리버 찰스 워드

47
당신은 효과적으로 "당신의 질문은 잘못된 것입니다.하지만 당신은 당신이 무엇을 알기 위해 답을 알 필요가 있다는 OP 말하는 거 해야합니다 요청했습니다." 사람들을 끄는 좋은 방법입니다. 또한 필요한 정보를 찾기가 어렵습니다. 사람들이 질문을하지 않는다 (닫는 효과적으로 "이 질문은 요청을받은 안된다"라고되어 있으면), 다음 사람에 대한 가능한 방법이 없을 것입니다 하지 않습니다 이미 답을 알고에 대한 답변을 얻기 위해 질문
jalf

7
@ jalf : 전혀 아닙니다. "투표 투표"는 "나쁜 질문"을 의미하지 않습니다. 나는 그것을 위해 투표를 사용합니다. 나는 대답이 "정의되지 않은 행동"인 모든 무수한 질문들 (i = i ++ 등)이 서로 중복된다는 의미에서 중복이라고 생각합니다. 다른 말로, 왜 질문이 중복되지 않으면 아무도 대답하지 않았습니까?
Oliver Charlesworth

5
@ jalf : Oli에 동의합니다. 질문은 중복되지 않지만 답변은 이미 답변이있는 다른 질문으로 리디렉션됩니다. 중복으로 닫힌 질문은 사라지지 않고 대신 답변이있는 다른 질문에 대한 포인터 역할을합니다. SSO를 찾는 다음 사람은 여기로 가서 리디렉션을 따르고 그녀의 답변을 찾습니다.
Matthieu M.

답변:


213

배경 / 개요

자동 변수 ( malloc/에서 호출하지 않고 만드는 변수 인 "스택에서" new)에 대한 작업은 일반적으로 사용 가능한 저장소 ( "힙",을 사용하여 만든 변수)와 관련된 작업보다 훨씬 빠릅니다 new. 그러나 자동 배열의 크기는 컴파일 타임에 고정되지만 무료 저장소의 배열 크기는 고정되지 않습니다. 또한 스택 크기는 제한적이며 (일반적으로 몇 MiB), 무료 저장소는 시스템 메모리에 의해서만 제한됩니다.

SSO는 Short / Small String Optimization입니다. A는 std::string일반적으로 통화에 것처럼 비슷한 성능 특성을 제공하는 무료 저장 ( "힙")에 대한 포인터로 문자열을 저장합니다 new char [size]. 이렇게하면 매우 큰 문자열의 스택 오버플로가 방지되지만 특히 복사 작업의 경우 속도가 느려질 수 있습니다. 최적화로서, 많은 구현은 std::string다음과 같은 작은 자동 배열 을 만듭니다 char [20]. 문자열이 20 자 이하인 경우 (이 예제에서는 실제 크기가 다름) 해당 배열에 직접 저장합니다. 이렇게하면 전화 new를 전혀 하지 않아도되므로 속도가 약간 빨라집니다.

편집하다:

나는이 답변이 그렇게 인기가있을 것으로 기대하지는 않았지만, 실제로는 "실제로"SSO의 구현을 전혀 읽지 않았다는 경고와 함께보다 현실적인 구현을하겠습니다.

구현 세부 사항

최소한 std::string다음 정보를 저장해야합니다.

  • 크기
  • 용량
  • 데이터의 위치

크기는 std::string::size_type또는 끝에 대한 포인터로 저장 될 수 있습니다 . 유일한 차이점은 사용자가 호출 할 때 두 개의 포인터를 빼야하는지 size또는 사용자가 호출 할 때 포인터 에 a size_type를 추가 해야하는지 end입니다. 용량은 어느 쪽이든 저장할 수 있습니다.

사용하지 않는 것에 대해서는 비용을 지불하지 않습니다.

먼저 위에서 설명한 내용을 기반으로 순진한 구현을 고려하십시오.

class string {
public:
    // all 83 member functions
private:
    std::unique_ptr<char[]> m_data;
    size_type m_size;
    size_type m_capacity;
    std::array<char, 16> m_sso;
};

64 비트 시스템의 경우 일반적으로 std::string문자열 당 24 바이트의 '오버 헤드'와 SSO 버퍼에 대한 16 바이트 (패딩 요구 사항으로 인해 20 대신 16이 선택됨)가 있음을 의미합니다. 간단한 예제 에서처럼이 세 가지 데이터 멤버와 로컬 문자 배열을 저장하는 것은 실제로 의미가 없습니다. 인 경우 m_size <= 16모든 데이터를에 넣을 m_sso것이므로 용량을 이미 알고 있으므로 데이터에 대한 포인터가 필요하지 않습니다. 인 경우 m_size > 16에는 필요하지 않습니다 m_sso. 내가 그들 모두를 필요로하는 곳은 겹치지 않습니다. 공간을 낭비하지 않는 똑똑한 솔루션은 다음과 같이 보일 것입니다 (예상되지 않은 예시 목적으로 만).

class string {
public:
    // all 83 member functions
private:
    size_type m_size;
    union {
        class {
            // This is probably better designed as an array-like class
            std::unique_ptr<char[]> m_data;
            size_type m_capacity;
        } m_large;
        std::array<char, sizeof(m_large)> m_small;
    };
};

대부분의 구현은 다음과 같다고 가정합니다.


7
다음은 실제 구현에 대한 좋은 설명입니다. stackoverflow.com/a/28003328/203044
BillT

대부분의 개발자가 const 참조를 사용하여 std :: string을 전달할 때 SSO가 실제로 실용적입니까?
Gupta

1
SSO는 복사 비용을 낮추는 것 외에 두 가지 이점이 있습니다. 첫 번째는 문자열 크기가 작은 버퍼 크기에 맞으면 초기 구성에 할당 할 필요가 없다는 것입니다. 두 번째는 함수가를 받아 들일 때 std::string const &데이터가 참조의 위치에 저장되기 때문에 데이터를 얻는 것이 단일 메모리 간접적이라는 것입니다. 작은 문자열 최적화가없는 경우 데이터에 액세스하려면 두 개의 메모리 간접 지정이 필요합니다 (먼저 문자열에 대한 참조를로드하고 해당 내용을 읽은 다음 두 번째는 문자열에서 데이터 포인터의 내용을 읽음).
David Stone

34

SSO는 "작은 문자열 최적화"의 약어로, 작은 문자열이 별도로 할당 된 버퍼를 사용하는 대신 문자열 클래스의 본문에 포함되는 기술입니다.


15

다른 답변에서 이미 설명했듯이 SSO는 Small / Short String Optimization을 의미 합니다. 이 최적화의 동기는 일반적으로 응용 프로그램이 긴 문자열보다 훨씬 짧은 문자열을 처리한다는 명백한 증거입니다.

데이비드 스톤 설명한 바와 같이 위의 그의 대답에서std::string클래스는 주어진 길이로 저장 내용에 대한 내부 버퍼까지를 사용하고,이을 제거해은 할 필요 동적으로 메모리를 할당합니다. 이것은 코드를 보다 효율적 이고 빠르게 만듭니다.

이 다른 관련 답변 은 내부 버퍼의 크기가 std::string구현에 따라 다르며 플랫폼마다 다릅니다 (아래 벤치 마크 결과 참조).

벤치 마크

다음은 길이가 같은 많은 문자열의 복사 작업을 벤치마킹하는 작은 프로그램입니다. 길이가 1 인 천만 개의 문자열을 복사하는 시간을 인쇄하기 시작합니다. 그런 다음 길이가 2 인 문자열로 반복합니다. 길이가 50이 될 때까지 계속 진행합니다.

#include <string>
#include <iostream>
#include <vector>
#include <chrono>

static const char CHARS[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
static const int ARRAY_SIZE = sizeof(CHARS) - 1;

static const int BENCHMARK_SIZE = 10000000;
static const int MAX_STRING_LENGTH = 50;

using time_point = std::chrono::high_resolution_clock::time_point;

void benchmark(std::vector<std::string>& list) {
    std::chrono::high_resolution_clock::time_point t1 = std::chrono::high_resolution_clock::now();

    // force a copy of each string in the loop iteration
    for (const auto s : list) {
        std::cout << s;
    }

    std::chrono::high_resolution_clock::time_point t2 = std::chrono::high_resolution_clock::now();
    const auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count();
    std::cerr << list[0].length() << ',' << duration << '\n';
}

void addRandomString(std::vector<std::string>& list, const int length) {
    std::string s(length, 0);
    for (int i = 0; i < length; ++i) {
        s[i] = CHARS[rand() % ARRAY_SIZE];
    }
    list.push_back(s);
}

int main() {
    std::cerr << "length,time\n";

    for (int length = 1; length <= MAX_STRING_LENGTH; length++) {
        std::vector<std::string> list;
        for (int i = 0; i < BENCHMARK_SIZE; i++) {
            addRandomString(list, length);
        }
        benchmark(list);
    }

    return 0;
}

이 프로그램을 실행 ./a.out > /dev/null하려면 문자열을 인쇄하는 시간이 계산되지 않도록해야합니다. 중요한 번호가에 인쇄되어 stderr콘솔에 표시됩니다.

MacBook 및 Ubuntu 컴퓨터의 출력으로 차트를 만들었습니다. 길이가 주어진 지점에 도달하면 문자열을 복사하는 데 시간이 크게 걸립니다. 이제 문자열이 더 이상 내부 버퍼에 맞지 않고 메모리 할당을 사용해야하는 순간입니다.

리눅스 머신에서는 줄 길이가 16에 도달하면 점프가 일어난다는 것에 주목하라. 맥북에서는 길이가 23에 도달하면 점프가 발생한다. 이는 SSO가 플랫폼 구현에 의존한다는 것을 확인시켜 준다.

우분투 우분투에서의 SSO 벤치 마크

맥북 프로 Macbook Pro의 SSO 벤치 마크

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.