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 に比べれば誤差の範囲かと思います。
http://github.com/shinh/test/blob/1f58c3c370e690595f3d2e0d12d4ed8e8aafab55/bench_string_piece.cc
完全に同じ感覚で使える、というほどのものではないものの、 std::string と似たようなインターフェイスをそれなりに持っているので、 std::string の allocation コストがバカにならない部分だけ移行する、というのもそれなりにラクにできるかと思います。特に、 StringPiece::substr なんかは、設計上当たり前ですが、 string::substr と違って allocation 不要なので、そういうことをよくするコードだと結構速くなるんじゃないかと思います。
まとめると、
- 仮引数の型を const std::string& にした場合と同じ程度にラクに書ける
- 仮引数を const std::string& にした場合と違って、引数として const char* を渡した場合に allocation のコストがかからないので、ほとんど overhead がない
- Chromium にまぎれこんでるので適当に使える
という感じでそれなりに気が効いてるいいものなので、使ってみてもいいんじゃないかと思います。