最適化とコードスタイルに関するC++の質問 では、std::string
。そのコンテキストでのSSOとはどういう意味ですか?
明らかに「シングルサインオン」ではありません。 「共有文字列の最適化」、おそらく?
自動変数(「スタックから」、malloc
/new
を呼び出さずに作成する変数)の操作は、一般に、フリーストア(「ヒープ」)を含む操作よりもはるかに高速です。 new
)を使用して作成される変数です。ただし、自動配列のサイズはコンパイル時に固定されますが、フリーストアの配列のサイズは固定されません。さらに、スタックサイズは制限されています(通常は数MiB)が、フリーストアはシステムのメモリによってのみ制限されます。
SSOは、Short/Small String Optimizationです。 std::string
は通常、フリーストア(「ヒープ」)へのポインターとして文字列を格納します。これにより、new char [size]
を呼び出す場合と同様のパフォーマンス特性が得られます。これにより、非常に大きな文字列のスタックオーバーフローが防止されますが、特にコピー操作の場合は遅くなる可能性があります。最適化として、std::string
の多くの実装は、char [20]
のような小さな自動配列を作成します。 20文字以下の文字列がある場合(この例では、実際のサイズは異なります)、その配列に直接格納されます。これにより、new
を呼び出す必要がまったくなくなり、速度が少し向上します。
編集:
私はこの答えがそれほど人気があるとは思っていませんでしたが、そうであるため、実際のSSOの実装を「実際に」読んだことがないという警告を付けて、より現実的な実装を提供しましょう。
少なくとも、std::string
は次の情報を保存する必要があります。
サイズは、std::string::size_type
または末尾へのポインタとして保存できます。唯一の違いは、ユーザーがsize
を呼び出したときに2つのポインターを減算するか、end
を呼び出したときにsize_type
をポインターに追加するかです。容量はどちらの方法でも保存できます。
最初に、上記で概説した内容に基づいた単純な実装を検討します。
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が選択されます)。私の簡単な例のように、これらの3つのデータメンバーとローカルの文字配列を格納するのはあまり意味がありません。 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;
};
};
ほとんどの実装はこのように見えると思います。
SSOは、「Small String Optimization」の略語で、個別に割り当てられたバッファを使用するのではなく、文字列クラスの本文に小さな文字列を埋め込む技術です。
他の回答で既に説明したように、SSOはSmall/Short String Optimizationを意味します。この最適化の背後にある動機は、アプリケーションが一般に長い文字列よりもはるかに短い文字列を処理するという否定できない証拠です。
デビッド・ストーンが説明したように 上記の彼の答えで 、std::string
クラスは内部バッファを使用して、指定された長さまでコンテンツを保存します。これにより、メモリを動的に割り当てる必要がなくなります。これにより、コードがより効率的におよびより高速になります。
この他の関連する答え は、内部バッファのサイズがstd::string
実装。プラットフォームごとに異なります(以下のベンチマーク結果を参照)。
以下は、同じ長さの多くの文字列のコピー操作のベンチマークを行う小さなプログラムです。長さ= 1の1000万文字列をコピーする時間の印刷を開始します。その後、長さ= 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のマシンからの出力でチャートを作成しました。長さが所定のポイントに達すると、文字列をコピーする時間に大きなジャンプがあることに注意してください。これは、文字列が内部バッファに収まらず、メモリ割り当てを使用する必要がある瞬間です。
Linuxマシンでは、文字列の長さが16に達するとジャンプが発生することに注意してください。Macbookでは、長さが23に達するとジャンプが発生します。これにより、SSOがプラットフォーム実装に依存することが確認されます。