J'utilisais une carte avec un std::string
clé et pendant que tout fonctionnait bien, je n'obtenais pas les performances que j'attendais. J'ai cherché des endroits pour optimiser et améliorer les choses un peu et c'est alors qu'un collègue a dit: "cette clé de chaîne va être lente."
J'ai lu des dizaines de questions et elles disent toujours:
"n'utilisez pas de
char *
comme clé "
"std::string
les clés ne sont jamais votre goulot d'étranglement "
"la différence de performances entre unchar *
et unstd::string
est un mythe. "
J'ai essayé à contrecœur un char *
et il y avait une différence, une grande différence.
J'ai réduit le problème à un exemple simple:
#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");
}
D'abord la version std :: string:
$ g++ -O3 -o test test.cpp -DUSE_STRING
$ time ./test 20000000
real 0m1.893s
Ensuite la version 'char *':
g++ -O3 -o test test.cpp
$ time ./test 20000000
real 0m0.465s
C'est une différence de performance assez importante et à peu près la même différence que je vois dans mon programme plus large.
Utilisant un char *
la clé est une douleur à gérer pour libérer la clé et ne se sent pas bien. Experts C++ qu'est-ce qui me manque? Des pensées ou des suggestions?
Vous utilisez un const char *
Comme clé de recherche pour find()
. Pour la carte contenant const char*
, C'est le type correct que find
attend et la recherche peut être effectuée directement.
La carte contenant std::string
S'attend à ce que le paramètre de find()
soit un std::string
, Dans ce cas, le const char*
Doit d'abord être converti en un std::string
. C'est probablement la différence que vous voyez.
Comme il a été noté, le problème est lié aux spécifications des conteneurs associatifs (ensembles et cartes), dans la mesure où leurs méthodes de recherche de membres forcent toujours une conversion en key_type
, même si un operator<
existe qui accepterait de comparer votre clé avec les clés de la carte malgré leurs différents types.
En revanche, les fonctions de <algorithm>
n'en souffre pas, par exemple lower_bound
est défini comme:
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 );
Ainsi, une alternative pourrait être:
std::vector< std::pair< std::string, int > >
Et puis vous pourriez faire:
std::lower_bound(vec.begin(), vec.end(), std::make_pair("hello", 0), CompareFirst{})
Où CompareFirst
est défini comme:
struct CompareFirst {
template <typename T, typename U>
bool operator()(T const& t, U const& u) const { return t.first < u.first; }
};
Ou même construire un comparateur entièrement personnalisé (mais c'est un peu plus difficile).
Un vector
de paire est généralement plus efficace dans les charges lourdes en lecture, c'est donc vraiment pour stocker une configuration par exemple.
Je conseille de fournir des méthodes pour encapsuler les accès. lower_bound
est assez bas niveau.
Si vous êtes en C++ 11, le constructeur de copie n'est pas appelé sauf si la chaîne est modifiée . Étant donné que std :: string est une construction C++, au moins 1 déréférence est nécessaire pour obtenir les données de chaîne.
Je suppose que le temps est pris dans une déréférence supplémentaire (qui si elle est effectuée 10000 fois est coûteuse), et std :: string effectue probablement des vérifications de pointeur nul appropriées, ce qui mange à nouveau des cycles.
Stockez la chaîne std :: en tant que pointeur, puis vous perdez la surcharge du constructeur de copie.
Mais après vous devez vous rappeler de gérer les suppressions.
La raison pour laquelle std :: string est lente est qu'elle se construit elle-même. Appelle le constructeur de copie, puis à la fin, appelle delete. Si vous créez la chaîne sur le tas, vous perdez la construction de la copie.
Après la compilation, les 2 littéraux de chaîne "Hello" auront la même adresse mémoire. Sur le char *
si vous utilisez ces adresses mémoire comme clés.
Dans le cas string
, chaque "Bonjour" sera converti en un objet différent. Il s'agit d'une petite partie (vraiment très petite) de votre différence de performances.
Une plus grande partie peut être que tous les "Bonjour" que vous utilisez ont la même adresse mémoire strcmp
obtiendra toujours 2 pointeurs de caractères équivalents et je suis sûr qu'il vérifie tôt ce cas :) Donc il n'itérera jamais vraiment tous les caractères mais la comparaison std :: string le fera.
Une solution à cela consiste à utiliser une classe de clé personnalisée qui agit comme un croisement entre un const char *
Et un std::string
, Mais a un booléen pour indiquer au moment de l'exécution s'il est "propriétaire" ou "non -propriété ". De cette façon, vous pouvez insérer une clé dans la carte qui possède ses données (et la libérera lors de la destruction), puis comparer avec une clé qui ne possède pas ses données. (Il s'agit d'un concept similaire au type Rust Cow<'a, str>
).
L'exemple ci-dessous hérite également de string_ref
De boost pour éviter d'avoir à réimplémenter des fonctions de hachage, etc.
REMARQUE, cela a pour effet dangereux que si vous insérez accidentellement dans la carte avec la version non propriétaire et que la chaîne que vous pointez sort de la portée, la clé pointe vers la mémoire déjà libérée. La version non propriétaire ne peut être utilisée que pour les recherches.
#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";
}