私はstd::string
キーを持つマップを使用していましたが、すべてが正常に機能している間、期待したパフォーマンスが得られませんでした。最適化と改善を行う場所を少しだけ探しましたが、そのとき、同僚は「その文字列キーは遅くなるだろう」と言いました。
私は何十もの質問を読み、彼らは一貫してこう言います:
「
char *
をキーとして使用しないでください」
"std::string
キーがボトルネックになることはありません"
「char *
とstd::string
のパフォーマンスの違いは神話です。」
しぶしぶchar *
キーを試してみましたが、違いがあり、大きな違いがありました。
問題を簡単な例に要約しました。
#include <stdio.h>
#include <stdlib.h>
#include <map>
#ifdef USE_STRING
#include <string>
typedef std::map<std::string, int> Map;
#else
#include <string.h>
struct char_cmp {
bool operator () (const char *a,const char *b) const
{
return strcmp(a,b)<0;
}
};
typedef std::map<const char *, int, char_cmp> Map;
#endif
Map m;
bool test(const char *s)
{
Map::iterator it = m.find(s);
return it != m.end();
}
int main(int argc, char *argv[])
{
m.insert( Map::value_type("hello", 42) );
const int lcount = atoi(argv[1]);
for (int i=0 ; i<lcount ; i++) test("hello");
}
まず、std :: stringバージョン:
$ g++ -O3 -o test test.cpp -DUSE_STRING
$ time ./test 20000000
real 0m1.893s
次に「char *」バージョン:
g++ -O3 -o test test.cpp
$ time ./test 20000000
real 0m0.465s
それはかなり大きなパフォーマンスの違いであり、私の大きなプログラムで見られるのとほぼ同じ違いです。
char *
キーを使用することは、キーの解放を処理するのが面倒で、気分が悪くなります。 C++の専門家には何が欠けていますか?考えや提案はありますか?
find()
の検索キーとして_const char *
_を使用しています。 _const char*
_を含むマップの場合、これはfind
が期待する正しいタイプであり、ルックアップは直接実行できます。
_std::string
_を含むマップは、find()
のパラメーターが_std::string
_であると想定しているため、この場合、最初に_const char*
_を_std::string
_に変換する必要があります。これはおそらくあなたが見ている違いです。
Sthが述べたように、問題は連想コンテナ(セットおよびマップ)の仕様の1つであり、そのメンバー検索メソッドは、受け入れられるkey_type
が存在する場合でも、常にoperator<
への変換を強制します。タイプが異なっていても、キーをマップ内のキーと比較します。
一方、<algorithm>
の関数はこの影響を受けません。たとえば、 lower_bound
は次のように定義されます。
template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );
template< class ForwardIt, class T, class Compare >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value, Compare comp );
したがって、代替案は次のようになります。
std::vector< std::pair< std::string, int > >
そして、あなたは次のことができます:
std::lower_bound(vec.begin(), vec.end(), std::make_pair("hello", 0), CompareFirst{})
CompareFirst
は次のように定義されます。
struct CompareFirst {
template <typename T, typename U>
bool operator()(T const& t, U const& u) const { return t.first < u.first; }
};
または、完全にカスタムのコンパレータを作成することもできます(ただし、少し難しくなります)。
ペアのvector
は一般に、読み取りが重いロードでより効率的であるため、たとえば設定を実際に保存することができます。
アクセスをラップする方法を提供することをお勧めします。 lower_bound
はかなり低レベルです。
C++ 11の場合、コピーコンストラクターは 文字列が変更されない限り と呼ばれません。 std :: stringはC++コンストラクトであるため、文字列データを取得するには少なくとも1つの間接参照が必要です。
私の推測では、余分な逆参照に時間がかかります(10000回実行するとコストがかかります)。
Std :: stringをポインターとして保存すると、コピーコンストラクターのオーバーヘッドがなくなります。
ただし、削除を処理することを忘れないでください。
Std :: stringが遅い理由は、それ自体が構造であるためです。コピーコンストラクターを呼び出し、最後にdeleteを呼び出します。ヒープ上に文字列を作成すると、コピー構造が失われます。
コンパイル後、2つの「Hello」文字列リテラルは同じメモリアドレスになります。 char *
このメモリアドレスをキーとして使用する場合。
string
の場合、すべての「Hello」は異なるオブジェクトに変換されます。これは、パフォーマンスの違いのごく一部(本当に小さい)です。
より大きな部分は、使用しているすべての「Hello」が同じメモリアドレスを持っているので、strcmp
は常に2つの同等のcharポインターを取得し、このケースを早期にチェックすることを確信しています:)すべての文字を実際に反復することはありませんが、std :: stringの比較は反復します。
これに対する1つの解決策は、const char *
とstd::string
の間のクロスとして機能するカスタムキークラスを使用することですが、実行時に「所有」または「非所有」かどうかを示すブール値「。そうすれば、データを所有するマップにキーを挿入し(そして破壊時に解放します)、データを所有していないキーと比較できます。 (これはRust Cow<'a, str>
タイプと同様の概念です)。
以下の例も、ブーストのstring_ref
を継承して、ハッシュ関数などを再実装する必要を回避しています。
注:これには、所有していないバージョンを誤ってマップに挿入し、ポイントしている文字列が範囲外になると、キーが既に解放されたメモリをポイントするという危険な影響があります。非所有バージョンは、ルックアップにのみ使用できます。
#include <iostream>
#include <map>
#include <cstring>
#include <boost/utility/string_ref.hpp>
class MaybeOwned: public boost::string_ref {
public:
// owning constructor, takes a std::string and copies the data
// deletes it's copy on destruction
MaybeOwned(const std::string& string):
boost::string_ref(
(char *)malloc(string.size() * sizeof(char)),
string.size()
),
owned(true)
{
memcpy((void *)data(), (void *)string.data(), string.size());
}
// non-owning constructor, takes a string ref and points to the same data
// does not delete it's data on destruction
MaybeOwned(boost::string_ref string):
boost::string_ref(string),
owned(false)
{
}
// non-owning constructor, takes a c string and points to the same data
// does not delete it's data on destruction
MaybeOwned(const char * string):
boost::string_ref(string),
owned(false)
{
}
// move constructor, tells source that it no longer owns the data if it did
// to avoid double free
MaybeOwned(MaybeOwned&& other):
boost::string_ref(other),
owned(other.owned)
{
other.owned = false;
}
// I was to lazy to write a proper copy constructor
// (it would need to malloc and memcpy again if it owned the data)
MaybeOwned(const MaybeOwned& other) = delete;
// free owned data if it has any
~MaybeOwned() {
if (owned) {
free((void *)data());
}
}
private:
bool owned;
};
int main()
{
std::map<MaybeOwned, std::string> map;
map.emplace(std::string("key"), "value");
map["key"] += " here";
std::cout << map["key"] << "\n";
}