提高 C++ 字符串性能:使用 reserve 方法预分配内存

发布于:2024-10-16 ⋅ 阅读:(150) ⋅ 点赞:(0)

在 C++ 编程中,字符串是一个常用的数据类型,而 std::string 类提供了丰富的功能来处理文本数据。然而,在频繁修改字符串的情况下,性能问题可能会显现出来。本文将探讨如何通过使用 reserve 方法预分配内存来提高 C++ 字符串的性能。

1. 理解 std::string 的内存管理

std::string 是一个动态字符串类,它会根据需要自动管理内存。当我们向字符串添加字符时,如果当前字符串的容量不足以容纳新字符,std::string 会自动分配更多的内存。本质上是因为 std::string 底层存储是一块连续的空间,与 std::vector 是一样的。这种动态内存分配虽然方便,但在频繁修改字符串时,会导致性能下降,特别是在多次分配和释放内存的情况下。

我们可以看一下 std::string 的源码,探究一下其底层实现,理解的就会更清晰一点了。
我的linux环境是gcc 4.8.5,c++的头文件位于/usr/include/c++
最终我们需要看的是这两个文件:
/usr/include/c++/4.8.5/bits/basic_string.h
/usr/include/c++/4.8.5/bits/basic_string.tcc
在basic_string.h中有这么一段

  *  A string looks like this:
  *
  *  @code
  *                                        [_Rep]
  *                                        _M_length
  *   [basic_string<char_type>]            _M_capacity
  *   _M_dataplus                          _M_refcount
  *   _M_p ---------------->               unnamed array of char_type
  *  @endcode
  *
  *  Where the _M_p points to the first character in the string, and
  *  you cast it to a pointer-to-_Rep and subtract 1 to get a
  *  pointer to the header.

我们可以大概了解到,string对象的存储布局是什么样子的。
先看string的data()成员,因为data()成员获取的是内存的地址。
代码我做了容易识别的优化,因为标准库的代码,看起来不像业务代码。

const char* data() const 
{ return _M_data(); }

char* _M_data() const
{ return  _M_dataplus._M_p; }

struct _Alloc_hider : _Alloc
{
_Alloc_hider(char* __dat, const _Alloc& __a)
: _Alloc(__a), _M_p(__dat) { }

char* _M_p; // The actual data.
};

private:
_Alloc_hider	_M_dataplus;

首先,data()成员是public的,它调用了_M_data()成员,
_M_data()这个可以很容易看出是个private的,因为是以下划线_开头的。
_M_data()成员返回的是_M_dataplus的_M_p。
_M_p就是一个简单的char指针。
所以,从上面的源码我们可以看到,
std::string 底层存储的数据,就是存在一个char*指向的内存区域。

2. 什么是 reserve 方法?

reserve 方法是 std::string 类提供的一种功能,用于预分配内存。通过调用 reserve(size_t n),我们可以告诉字符串对象预留至少 n 个字符的空间。这意味着在添加字符时,如果新字符的数量不超过预留的空间,就不需要重新分配内存,从而减少了性能开销。

示例代码:

#include <iostream>
#include <string>

int main() {
    std::string str;
    str.reserve(100);  // 预分配 100 个字符的空间

    for (int i = 0; i < 50; ++i) {
        str += 'a';  // 添加字符
    }

    std::cout << "字符串长度: " << str.length() << std::endl;
    std::cout << "字符串容量: " << str.capacity() << std::endl;

    return 0;
}

在这个示例中,我们预先为字符串分配了 100 个字符的空间。接下来,我们在循环中添加了 50 个字符。由于我们预留了足够的空间,std::string 不需要进行额外的内存分配,这样可以显著提高性能。

那 reserve 方法的源码是什么样子的?
下面摘出来的代码,也是简化过的方便阅读了。

    void reserve(size_type __res)
    {
      // 检查请求的容量 __res 是否与当前字符串的容量不同,或者当前字符串是否是共享的(即多个 std::string 对象可能共享同一块内存)。如果满足任一条件,则需要进行内存重新分配。
      if (__res != this->capacity() || _M_rep()->_M_is_shared()) {
		  // 确保不会缩小容量,也就是不能影响当前已经存储的数据
		  if (__res < this->size())
		    __res = this->size();
		  const allocator_type __a = get_allocator();
		  // 使用分配器 __a 分配新的内存,大小为 __res - this->size()。
		  // __res - this->size() 是需要多分配的内存大小
		  // _M_clone() 方法会再加上当前大小,得到最终的容量
		  _CharT* __tmp = _M_rep()->_M_clone(__a, __res - this->size());
		  // 调用 _M_dispose() 方法释放当前字符串的旧内存
		  _M_rep()->_M_dispose(__a);
		  _M_data(__tmp);
        }
    }
   
    char* _M_clone(const _Alloc& __alloc, size_type __res)
    {
      // 这个 __requested_cap 就是最终要申请的内存大小
      const size_type __requested_cap = this->_M_length + __res;
      _Rep* __r = _Rep::_S_create(__requested_cap, this->_M_capacity, __alloc);
      if (this->_M_length)
		_M_copy(__r->_M_refdata(), _M_refdata(), this->_M_length);
      __r->_M_set_length_and_sharable(this->_M_length);
      return __r->_M_refdata();
    }

__res != this->capacity()
这第一个条件很好理解,
目标容量与当前容量不一样的时候,那肯定要重新分配内存了

主要是第二个条件,如果当前字符串是否是共享的,那么也要重新分配内存
_M_rep()->_M_is_shared()
这个是因为,string::string 本身是支持多个字符串对象共享同一块内存的。你调用 reserve() 方法就是为了后面要进行相关内存操作了。所以,单独重新分配内存了,保证后续的内存操作不影响其他共享内存的string对象。

3. 何时使用 reserve?

在以下情况下,使用 reserve 方法是一个不错的选择:

已知字符串长度 :如果你知道最终字符串的长度,使用 reserve 可以避免多次内存分配。
频繁修改字符串 :在需要频繁追加字符的场景下,使用 reserve 可以提高性能,降低内存碎片的风险。
性能敏感的应用 :在对性能有严格要求的应用中,合理使用 reserve 可以显著减少运行时的开销。

4. 注意事项

虽然 reserve 方法可以提高性能,但也有一些需要注意的地方:

内存浪费 :如果预留的空间超过了实际需要的空间,会导致内存的浪费。因此,合理估计字符串的最终长度是关键。
不缩小容量 :调用 reserve 不会改变字符串的长度。如果你需要减少字符串的容量,可以使用 shrink_to_fit 方法。
动态变化 :在某些情况下,字符串的长度可能会动态变化,因此在每次修改前都调用 reserve 可能并不合理。

5. 结论

在 C++ 中,合理使用 std::string 的 reserve 方法可以显著提高字符串操作的性能。通过预分配内存,我们可以避免频繁的内存分配和释放,从而提升程序的运行效率。对于性能敏感的应用,尤其是在处理大量字符串操作时,使用 reserve 是一种简单而有效的优化策略。

在实际开发中,在编写代码时最好是要考虑到字符串的内存管理,特别是在需要频繁追加字符串的时候。通过掌握这个技巧,你能够编写出更高效的 C++ 代码。


网站公告

今日签到

点亮在社区的每一天
去签到