C ++ map <std :: string> vs map <char *>性能(我知道,还是“?”)

我使用的地图带有char *键,并且在一切正常的情况下,我没有获得预期的性能。 我只搜索了一些地方来优化和改进某些东西,那时同事说:“字符串键会变慢。”

我读了几十个问题,他们总是说:

“不要使用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 ++专家我缺少什么? 有什么想法或建议吗?

uroc asked 2020-08-10T19:33:11Z
6个解决方案
26 votes

您将const char *用作std::string的查找键。对于包含find()的映射,这是std::string期望的正确类型,可以直接完成查找。

包含std::string的映射期望find()的参数为std::string,因此在这种情况下,首先必须将const char*转换为std::string。这可能就是您所看到的差异。

sth answered 2020-08-10T19:33:21Z
8 votes

如前所述,问题是关联容器(集合和地图)的规范之一,因为它们的成员搜索方法始终会强制转换为lower_bound,即使存在operator<愿意接受将您的密钥与其中的密钥进行比较 尽管类型各异的地图。

另一方面,lower_bound中的功能不受此影响,例如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{})

其中lower_bound定义为:

struct CompareFirst {
     template <typename T, typename U>
     bool operator()(T const& t, U const& u) const { return t.first < u.first; }
};

甚至构建一个完全自定义的比较器(但要困难一些)。

成对的lower_bound通常在读取重负载中效率更高,因此,例如,它实际上是存储配置的。

我确实建议提供包装访问的方法。 lower_bound是相当底层的。

Matthieu M. answered 2020-08-10T19:34:12Z
3 votes

如果您使用的是C ++ 11,则除非更改字符串,否则不会调用复制构造函数。 由于std :: string是C ++构造,因此至少需要进行1次解引用才能获取字符串数据。

我的猜测是时间会花费在额外的取消引用中(如果执行10000次会很昂贵),并且std :: string可能会进行适当的空指针检查,这又会占用很多周期。

sevensevens answered 2020-08-10T19:34:37Z
1 votes

将std :: string存储为指针,然后会损失复制构造函数的开销。

但是之后您必须记住要处理删除操作。

std :: string速度慢的原因是它本身在构造。 调用复制构造函数,然后最后调用delete。 如果在堆上创建字符串,则会丢失副本构造。

Adrian Cornish answered 2020-08-10T19:35:06Z
1 votes

编译后,2个“ Hello”字符串文字将具有相同的内存地址。 在strcmp的情况下,您可以将此存储地址用作键。

strcmp的情况下,每个“ Hello”都将转换为不同的对象。 这只是性能差异的一小部分(真的很小)。

更大的部分是,由于您使用的所有“ Hello”都具有相同的内存地址strcmp始终会获得2个等效的char指针,因此我很确定它会尽早检查这种情况:)因此,它永远不会真正进行迭代 除std :: string以外的所有字符都将比较。

QwerJoe answered 2020-08-10T19:35:35Z
0 votes

一种解决方案是使用自定义键类,该键类充当string_refstd::string之间的交叉,但在运行时具有一个布尔值来告知它是“拥有”还是“非拥有”。 这样,您可以将一个密钥插入拥有它的数据的地图(并在销毁时将其释放),然后与一个不拥有它的数据的钥匙进行比较。 (这与防锈Cow<'a, str>类型相似)。

以下示例还继承了boost的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";
}
Drgabble answered 2020-08-10T19:36:05Z
translate from https://stackoverflow.com:/questions/12136071/c-mapstdstring-vs-mapchar-performance-i-know-again