StringPiece というライブラリの話
例えばこう、ディレクトリの名前とその中のファイル名を / でくぎって結合する関数を書くとします。引数が std::string でも使いたいし const char* でも使いたい、ということで、たいていは
void JoinFilePathStr(const string& dir, const string& base, string* out) {
out->clear();
out->append(dir);
out->push_back('/');
out->append(base);
}なんてのを書くんじゃないかと思います。この関数で問題になるのは const char* を渡すと不要な string object が一度できることで、敬虔な C++ 屋さんだと、
void JoinFilePathStr(const string& dir, const char* base, string* out); void JoinFilePathStr(const char* dir, const string& base, string* out); void JoinFilePathStr(const char* dir, const char* base, string* out);
などと 3 パターン用意したりするかもしれません。パフォーマンスとかを考えると、 std::string の場合は文字列サイズを求めるために strlen する必要が無いとかそういう理由から、
void JoinFilePathStr(const char* dir, size_t dir_len,
const char* base, size_t base_len,
string* out);なんてのも用意して、これを他の4つから使うことになるのかなぁ、と思います。めんどくさいです。
こういうものを解決するために Google でよく使われているのが、 StringPiece という小さいクラスです。文字列の実体の所有権は持ってないけど、文字列の先頭へのポインタとサイズを持っている、みたいな物体で、他の opensource プロジェクトに混じってリリースされたりもしています。例えば Chromium のやつはここにあります。
http://src.chromium.org/viewvc/chrome/trunk/src/base/string_piece.h
この StringPiece は std::string や const char* から暗黙の変換で変換されるようになっているため、関数のプロトタイプでこれを引数としておくと、 std::string で受けた場合と同様に std::string が入力として与えられても、 const char* が入力であっても呼び出せる感じになります。具体的にはこんな感じ。
void JoinFilePathSp(const StringPiece& dir, const StringPiece& base,
string* out) {
dir.CopyToString(out);
*out += '/';
base.AppendToString(out);
}
int main() {
const string& dir = "/tmp";
const string& base = "hoge.c";
string joined;
// 以下はどれも結果は同じ
JoinFilePathSp(dir, base, &joined);
JoinFilePathSp("/tmp", base, &joined);
JoinFilePathSp(dir, "hoge.c", &joined);
JoinFilePathSp("/tmp", "hoge.c", &joined);
}適当に std::string で受けた場合と比較してベンチマークしてみます。
#define BENCH(msg, expr) do { \
joined.clear(); \
time_t start = clock(); \
for (int i = 0; i < 1000000; i++) { \
expr; \
} \
int elapsed = clock() - start; \
assert(!strcmp(joined.c_str(), "/tmp/hoge.c")); \
printf("%s %f\n", msg, (double)elapsed / CLOCKS_PER_SEC); \
} while (0)
int main() {
const string& dir = "/tmp";
const string& base = "hoge.c";
string joined;
BENCH("Str(const char*, const char*)",
JoinFilePathStr("/tmp", "hoge.c", &joined));
BENCH("Str(string, const char*)",
JoinFilePathStr(dir, "hoge.c", &joined));
BENCH("Str(const char*, string)",
JoinFilePathStr("/tmp", base, &joined));
BENCH("Str(string, string)",
JoinFilePathStr(dir, base, &joined));
BENCH("Sp(const char*, const char*)",
JoinFilePathSp("/tmp", "hoge.c", &joined));
BENCH("Sp(string, const char*)",
JoinFilePathSp(dir, "hoge.c", &joined));
BENCH("Sp(const char*, string)",
JoinFilePathSp("/tmp", base, &joined));
BENCH("Sp(string, string)",
JoinFilePathSp(dir, base, &joined));
}結果は、
Str(const char*, const char*) 0.250000 Str(string, const char*) 0.140000 Str(const char*, string) 0.140000 Str(string, string) 0.050000 Sp(const char*, const char*) 0.060000 Sp(string, const char*) 0.060000 Sp(const char*, string) 0.050000 Sp(string, string) 0.060000
という感じになりました。 const char* => std::string の変換が無いぶん、最初3つのケースでは StringPiece の方が速くなっています。このベンチマークでは allocation のコストがだいたい律速する感じのようなので、だいたい const char* => std::string の変換が一度も起きていない、 Str(string, string) と Sp(*, *) が同じような結果になっています。
同時に、 std::string で受けるのと同じように一つのバージョンだけ書けば良くてラクなので、まぁなにかと便利な物体だったりします。
今回のベンチマークのコードはこのへんにあります。ちなみに、 StringPiece::AppendToString は string::append(const string&) じゃなくて string::append(const char*, size_t) を使うようで、そのぶん Str(string, string) のケースは少しだけ Sp(string, string) のケースより速い傾向にあるみたいでした、がまぁ allocation に比べれば誤差の範囲かと思います。
完全に同じ感覚で使える、というほどのものではないものの、 std::string と似たようなインターフェイスをそれなりに持っているので、 std::string の allocation コストがバカにならない部分だけ移行する、というのもそれなりにラクにできるかと思います。特に、 StringPiece::substr なんかは、設計上当たり前ですが、 string::substr と違って allocation 不要なので、そういうことをよくするコードだと結構速くなるんじゃないかと思います。
まとめると、
- 仮引数の型を const std::string& にした場合と同じ程度にラクに書ける
- 仮引数を const std::string& にした場合と違って、引数として const char* を渡した場合に allocation のコストがかからないので、ほとんど overhead がない
- Chromium にまぎれこんでるので適当に使える
という感じでそれなりに気が効いてるいいものなので、使ってみてもいいんじゃないかと思います。