【问题标题】:Limit number of std::string allocations限制 std::string 分配的数量
【发布时间】:2015-02-05 15:13:25
【问题描述】:

我有一个函数,它从带有两个数字的const char* 构造一个std::string,作为参数传递,附加到它的末尾。

std::string makeName(const char* name, uint16_t num1, uint16_t num2) {

    std::string new_name(name);
    new_name.reserve(new_name.length()+5);

    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num1);
    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num2);

    return new_name;
}

这个函数被调用数千次,为分配在堆上的小对象创建唯一的名称。

Object* object1= new Object(makeName("Object", i, j)); // i and j are simply loop indices

我发现使用 valgrind 的 massif 工具调用 makeName 会分配大量内存,因为它被调用了很多次。

87.96% (1,628,746,377B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc.

->29.61% (548,226,178B) 0xAE383B7: std::string::_Rep::_S_create(unsigned long, unsigned long, std::allocator<char> const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)

| ->26.39% (488,635,166B) 0xAE38F79: std::string::_Rep::_M_clone(std::allocator<char> const&, unsigned long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)

| | ->26.39% (488,633,246B) 0xAE39012: std::string::reserve(unsigned long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19)

| | | ->15.51% (287,292,096B) 0x119A80FD: makeName(char const*, unsigned short, unsigned short) (Object.cpp:110)

| | | | ->15.51% (287,292,096B) in 42 places, all below massif's threshold (01.00%)

我的问题是,如何最大限度地减少这些分配以帮助减少程序使用的总内存量?

编辑: 我还想指出,作为程序要求,我不能使用 c++11 功能。

【问题讨论】:

  • 尝试使用字符串流。 sstream ss; ss &lt;&lt; name &lt;&lt; " : " &lt;&lt; num1 &lt;&lt; " : " &lt;&lt; num2; return ss.str();
  • “名称”必须是std::string 吗?如果您的对象数量在数字限制范围内,如何为其分配一个唯一的int
  • @NathanOliver 很好的第一步:但如果有问题的代码是一个严重的瓶颈,那么转到stringstream 不是答案。
  • @akashPradhan - 这是一个很好的建议,但是对象的名称通常不是“对象”——它将是对象的描述性名称
  • 哦,我们在谈论多少个对象?名称"Object:22:979" 将占用 14 字节的内存,加上 12-24 字节用于跟踪它的指针,以及另外 4-16 字节的堆分配开销。如果这与您的对象相比很大,并且您有很多对象......那就是开销。如果对象很小,那么高百分比可能是因为这是您要求的?

标签: c++ valgrind allocation stdstring


【解决方案1】:

在这种情况下,只有使用sprintf 的 DIY 自定义转换才有效。

所以我会使用 sprintfMEASURE

只有当这还不够好时,我才会实现自己的整数到字符串(从许多案例中知道它肯定会更快一些,但不足以证明从那开始是合理的)。


示例。而不是当前的高级代码

std::string makeName(const char* name, uint16_t num1, uint16_t num2) {

    std::string new_name(name);
    new_name.reserve(new_name.length()+5);

    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num1);
    new_name += ":";
    new_name += boost::lexical_cast<std::string>(num2);

    return new_name;
}

只是做

auto makeName( const char* const name, const uint16_t num1, const uint16_t num2 )
    -> std::string
{
    std::string result( strlen( name ) + 25, '\0' );    // 25 is large enough.
    int const n = sprintf( &result[0], "%s:%d:%d", name, num1, num2 );
    result.resize( n );
    return result;
}

免责声明:编译器未触及代码。

【讨论】:

  • 取决于编译器,Boost.Spirit.Karma handily beats sprintf。还有 FastFormat 库,但我从未使用过。
  • @KonradRudolph:感谢您参考 FastFormat。所以我今天也学到了一个新东西。还没死! :)
  • std::string 现在保证是连续的吗?我认为 &v[0] 只有使用矢量才是安全的
  • @KennyOstrom:std::string 的缓冲区自 2005 年 Lillehammer 会议以来一直保证是连续的,我认为是。现在大约有十年了。 ;-)
  • @Mgetz:您的陈述存在许多问题,确实需要更长的讨论时间(例如,从 C++03 到 C++11 发生了变化)。但这里只需注意上面的代码不依赖于普通缓冲区内容的空终止。初始化为零只是因为它违反了良好的编程习惯,引入了不必要的低效率,甚至是微低效率,但除了缓冲区也可以被初始化为例如# 个字符。
【解决方案2】:

“我的问题是,我怎样才能最小化这些分配”

我突然想到你有这些名字的原因。您可以在需要时计算它们,而不是总是在构造函数中生成名称吗?那将是最好的改进 - 除非需要,否则不要这样做。

如果你碰巧已经有一个虚拟基础,并且类类型决定了字符串,那就很容易了。否则,枚举类型可以替换字符串,并且您有一个查找表。

Object* object1= new Object(i, j));
std::string Object::getName(){ compute here }

如果这没有帮助,那么您实际上确实需要每个对象的字符串,并且您只能通过优化该函数来获得小幅加速。我注意到您以一种大小构造字符串,然后再将其增长。您也可以使用 char 缓冲区,然后将其分配给成员字符串(在构造函数中)。

【讨论】:

  • 是的,很遗憾,我无法在需要时计算名称,我必须将它们存储在对象中,以便我可以通过其唯一名称引用该对象。
【解决方案3】:

是的,您的代码进行了大量分配。分析分配:

std::string new_name(name); // 1
new_name.reserve(new_name.length()+5); // 2

new_name += ":";
new_name += boost::lexical_cast<std::string>(num1); // possibly 4 (boost + operator+=)
new_name += ":"; // possibly 5
new_name += boost::lexical_cast<std::string>(num2); // possibly 7

'可能',因为它取决于数字所需的字符(越高,越多)。

如果您真的关心内存分配,asprintf(虽然不是标准)或您的版本(基于 s(n)printf 的返回值)可能是最佳选择:

std::string makeName(const char* name, uint16_t num1, uint16_t num2)
{
     char *ptr = nullptr; 
     int size  = asprintf( &ptr, "%s:%u:%u", name, num1, num2);
  return std::string(ptr, size); // to avoid copying the chars
}  

注意:正如@Cheersandhth.-Alf 指出的那样,如果std::string 分配内存失败,ptr 将是 ptr 被泄露。解决此问题的最佳方法是使用 std::unique_ptr,但我会让您根据自己的需要来解决。

如果您不想使用asprintf,您可以使用std::snprintf 获得类似的行为

std::string makeName(const char* name, uint16_t num1, uint16_t num2)
{
    int length = std::snprintf(nullptr, 0, "%s:%u:%u", name, num1, num2);

    if (length > 0 )
    {
        std::string new_name(length + 1, '\0');
        std::snprintf(&new_name[0], new_name.length(), "%s:%u:%u", name, num1, num2);

       return new_name;
    } 
    // else handle failure
} 

与您的版本(我没有使用boost::lexical_cast,但std::to_string)的差异非常大:运行500 次第一个版本使用72,890 字节,而第二个只使用23,890 字节! (用 valgrind memcheck 测量)

【讨论】:

  • –1 显示的代码不起作用,如果“更正”将不起作用。这段代码的基本思想,在&amp;(s.data()) 中存储一个指向新缓冲区的指针,甚至没有意义。添加一个非标准函数。
  • @Cheersandhth.-Alf 你是对的,我也意识到了这一点。修复。
  • 不错的更新。我暂时放弃我的反对意见,因为中间的例子泄漏了内存,而第三个例子不必要地导致效率低下(做两次工作),这是一种错误的尝试削减动态分配的内存的几个字节。后者不是 IMO 会证明否决票的错误,并且内存泄漏是临界的,但总而言之,我认为这意味着这个答案比指导更具误导性,因此我对此做出了决定。
  • 好吧,因为 uints 最多可以有 6 个字符,所以您只能计算字符串长度 + 12。但是,AFAIK,这是为 s(n)printf 分配正确空间的最佳方法。我将记录内存泄漏,让 OP 对此进行推理,以便他能够满足他的要求(他不能使用 C++11)。
  • 请注意,没有办法将缓冲区指定到std::string,除非从临时位置移动缓冲区。采用指针的构造函数不获取参数缓冲区的所有权。它复制。
猜你喜欢
  • 2022-06-29
  • 2020-11-04
  • 1970-01-01
  • 1970-01-01
  • 2016-07-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-21
  • 1970-01-01
相关资源
最近更新 更多