如何实现仅在堆栈上分配的字符串

How to implement a string that solely allocates on the stack

提问人:sbi 提问时间:10/14/2014 最后编辑:sbi 更新时间:10/14/2014 访问量:2049

问:

在大约十年前的一个项目中,我们发现 的动态分配导致了严重的性能消耗。在这种情况下,它分配了许多小向量,因此快速的解决方案是编写一个类似向量的类,该类包装在基于堆栈的预分配数组周围,用作其容量的原始存储。结果是.如果你知道一些基础知识,这样的东西很容易写出来,而且你可以在网上找到不少这样的野兽。事实上,boost 现在也有一个std::vectorcharstatic_vector<typename T, std::size_t Max>

现在在嵌入式平台上工作,我们恰好需要一个.这将是一个字符串,用于在堆栈上预先分配固定的最大内存量,并将其用作其容量。static_basic_string

起初我认为这应该相当容易(毕竟它可以基于现有的),但是再看一下界面我就不太确定了。它比 的界面复杂得多。特别是实现功能系列不仅仅是一项繁琐的任务。static_vectorstd::basic_stringstd::vectorfind()std::basic_string

这让我再次思考。毕竟,这就是创建分配器的目的:基于其他方式替换分配。但是,如果说分配器接口笨拙,那就太轻描淡写了。有几篇文章对此进行了解释,但在过去的 15 年里,我看到的本土分配器很少是有原因的。newdelete

所以这是我的问题:

如果您必须实现相似的,您会怎么做?basic_string

  • 写你自己的?static_basic_string
  • 写一个分配器要传递给 ?std::basic_string
  • 做一些我没有想到的事情?
  • 使用我不知道的 boost 中的东西?

与往常一样,我们有一个相当重要的限制:在嵌入式平台上,我们绑定到 GCC 4.1.2,因此我们只能使用 C++03、TR1 和 boost 1.52。

C++ 字符串 堆栈分配

评论

0赞 Marco A. 10/14/2014
这可能更适合 programmers.se 因为它完全处于设计阶段
0赞 sbi 10/14/2014
@Marco:嗯,看看 programmers.stackexchange.com/help/on-topic,“软件架构和设计”确实列在那里。然而。1)它被埋没在许多其他与我的问题无关的话题中,似乎表明他们在一个非常不同的层面上谈论架构,2)我不认为这是一个纯粹的设计问题,因为接口是固定的(它是接口),我更多的是问如何实现它。是的,这很模糊,但我确实相信这个问题更属于这里而不是那里。ICBWT。std::basic_string
2赞 ArunasR 10/14/2014
为什么你认为分配器如此可怕?..我用过几次它们;毕竟,这就是他们的目的。从“basic_string”派生“stack_string”,包含内置缓冲区的分配器作为成员,将该分配器传递给basic_string ctor。
0赞 Angew is no longer proud of SO 10/14/2014
动态分配是完全不可能的,还是可以接受每个实例执行一个动态分配,而不管其内容以后如何变化?std::basic_string
1赞 Tony Delroy 10/14/2014
@CouchDeveloper/@LightnessRacesinOrbit:考虑具有许多小字符串成员的 - 使它们在内存中几乎连续 - 通常在同一页缓存上 - 与将每个指针存储在一起相比,听起来非常有利 - 即使来自同一个自定义池 - 避免“不连续”分配而没有其他不需要的协调开销。struct

答:

1赞 user1095108 10/14/2014 #1

很简单,写一个堆栈分配器,下面是一个例子:

https://codereview.stackexchange.com/questions/31528/a-working-stack-allocator

使用分配器,您可以同样轻松地进行分配,例如,从内存映射文件(即从磁盘驱动器)或静态数组 s 进行分配。char

评论

1赞 Audrius Meškauskas 10/14/2014
此解决方案在自定义堆栈数据结构中分配字符串,而不是在编译器用于记住返回地址的真正堆栈上分配字符串。它仍然可以作为一种解决方案。我投赞成票。
1赞 ArunasR 10/14/2014
此代码采用 C++14 语言。要求是C++03......尽管如此,原则是可以重写的。请注意,std::basic_string 以奇怪而奇妙的方式使用分配器,因此您必须添加比较运算符之类的内容。另请注意,std::basic_string(和其他容器)大量使用分配器的复制构造函数,并且不能优雅地处理它。stack_store
0赞 user1095108 10/14/2014
@arunasr 它是 c++11 代码。至少这是我编译它的原因。
0赞 ArunasR 10/14/2014
@user1095108 关键是,它不是 C++03/C++98。如果与 basic_string 一起使用,它将不会编译,因为它没有实现 和 .我不确定它将如何处理复制操作,但从外观上看,它不会很漂亮。operator==struct rebind
1赞 Emilio Garavaglia 10/14/2014 #2

有很多实现,有些完全基于动态分配,有些则仅针对宽度超过给定长度的字符串进行动态分配(实际上,它们在适合时使用自己的内部缓冲区)。basic_string

使用分配器可能不是办法,因为字符串和分配器之间的接口假定分配器对象是容器的一部分,但分配的内存来自容器本身的外部。您可以通过使用 POSIX 实现分配器来安排它,但存在所有缺点alloca

在堆栈上实现字符串时的问题在于,你不能让它动态增长(可能是当时的堆栈有更多的东西),但你也必须注意这样的操作,这样可以使字符串越来越长。+=

因此,您最终会预先分配(作为数组或分配器提供的缓冲区,在您的类中或在分配器中不会改变问题)您通常会浪费的字节数量,但不会全部填充它们,或者如果字符串增长太多并且需要动态,则不使用它们。

内存到缓存的通信过程(通常以 128 字节或 4KB 运行)可能需要权衡取舍,但它非常依赖于硬件,因此负担的复杂性很可能不会付出代价。

一个更经济实惠的解决方案可以是一个分配器,它仍然在堆上分配,但能够保留和重用返回的块(达到一定的限制),从而减少要求系统分配/解除分配的需要。

但是,在这种情况下,如果底层系统已经以这种方式实现,则性能不一定受益。new/delete

评论

0赞 sbi 10/14/2014
是的,这当然需要过度分配。但是,有问题的字符串通常限制为 16 或 32 个字符,因此这应该不是问题。此外,这一切都是为了与 C API 接口,该 API 假定预先分配的缓冲区,这些缓冲区不得重新分配,因此也是整体分配。
0赞 Emilio Garavaglia 10/14/2014
如果是这种情况,您需要一个size_t和一个可以包含 32 个字符或指针的空格(鉴别器是 )。由于数据的特殊性,您可能会使它成为一个独立的自包含类,最终可隐式转换为 ad 。将自定义分配器与basic_string一起使用可能很困难:如果basic_string(假设数据在其外部并且可以移动)尝试“移动”,会发生什么?size()<32std::string
0赞 sbi 10/14/2014
您描述的是 SSO。但我们不能使用它,因为它依赖于动态内存——我们在这里不能使用它。
0赞 sbi 10/14/2014
是的,看起来是这样。嗯,这就是我现在正在做的事情。
1赞 Abyx 10/14/2014 #3

LLVM ADT 具有 SmallString 类。它还具有许多其他有用的类。SmallVector

虽然当前的LLVM代码库倾向于使用C++11,但(不那么)旧版本的LLVM支持C++03。

评论

0赞 sbi 10/14/2014
有趣。这可以用作 ?std::basic_string
0赞 Abyx 10/14/2014
@sbi它的界面看起来与basic_string的类似,但它没有一些构造函数
0赞 sbi 10/14/2014
嗯。我宁愿有一个我可以放进去的东西,重新编译,然后完成。(正如我所写的,界面是 。无论如何,谢谢!std::basic_string
0赞 Lightness Races in Orbit 10/14/2014 #4

我应该考虑,我会使用实现定义的 VLA 和标准算法的组合。

4赞 James Kanze 10/14/2014 #5

第一个问题是:你有多少额外的接口 用?大多数附加接口可以 使用 (例如 , 和 ) 中的函数轻松实现,并且在很多 案例中,有很大一部分无论如何都不会被使用。 只需根据需要实现它即可。std::string<algorithm>std::findstd::find_ifstd::search

我不认为您可以使用自定义分配器来完成这项工作。 获取内存“堆栈”的唯一方法是声明它 作为自定义分配器的成员,这将创建所有 复制它们时会出现各种问题。分配器必须是 可复制,并且副本必须是幂等的。

也许你可以在网上找到一个免费的实现,它使用小字符串实现;然后 修改它,使截止大小(超过该截止大小,它使用动态 allocation) 大于您实际使用的任何字符串。(那里 是标准库的几个开源实现 可用;与 g++ 一起提供的那个仍然使用 COW,但 我怀疑其他大多数人都使用 SSO。std::string

评论

0赞 leemes 10/14/2014
关于自定义分配器:他写道,他想包装一个现有的堆栈分配缓冲区,这应该是可能的,对吧?
1赞 James Kanze 10/14/2014
@leemes也许吧。仍然存在范围问题:当他返回一个字符串时,副本将使用与被复制的内容相同的分配器。如果这反过来又使用堆栈分配的缓冲区,他将陷入深深的麻烦。
0赞 sbi 10/14/2014
事实上,我不知道如何让分配器使用本地堆栈空间,但我想知道是否有人可以指出一个简单的方法。关于寻找使用 SSO 的实现:正如我所说,我并不担心内存管理。我已经(为此)实现了这一点,我甚至可以在此基础上实现它,而不是再次实现它。static_vectorstatic_basic_string
0赞 sbi 10/14/2014
你建议放弃界面中更难做的部分,即家庭,对我来说似乎已经足够好了。我刚刚问了今天在座的所有开发人员,他们上一次使用字符串的 find 函数是什么时候,结果发现有些人从未使用过它们,有些人十年一次。所以我想我现在会跳过它们。find()
2赞 James Kanze 10/14/2014
@sbi我也差不多,我做了很多解析。在99%的情况下,我只使用中的函数。同样,类似或似乎也很少见的事情;我通常会建立一个副本,然后只是附加。<algorithm>insertreplace
1赞 Tony Delroy 10/14/2014 #6

一个很好的起点是 Alexandrescu 的基于策略的字符串类,在这篇 Dobbs 博士的文章中进行了描述。它包括一个 SSO 策略,该策略基本上可以执行您想要的操作(在页面中搜索),并且如果您认为有必要,可以轻松修改。它早于 C++11,所以你也很好。SmallStringOpt

评论

0赞 sbi 10/14/2014
为此,我介绍了内存管理的内在原理。正是对其他一些操作的重写让我质疑我的方法。
0赞 Tony Delroy 10/14/2014
@sbi“重写其他一些操作”——当您使用 SSO 策略实例化 Alexandrescu 的模板时,您将获得所有其他样式的操作。这怎么不能解决您的担忧?std::string
0赞 sbi 10/14/2014
啊,好吧,如果这是 的直接替代品,这应该有效。谢谢,我会看看的。std::basic_string
0赞 Tony Delroy 10/14/2014
@sbi 是的 - 第一段 - “......文章提供并解释了一个基于策略的实现,它为您提供了 12 个符合标准的预制实现,这些实现具有各种优化和权衡......”。干杯。basic_string
0赞 sbi 10/14/2014
谢谢,我确实可以从那里“借用”这些实现()。然而,正如詹姆斯所指出的,我也可以暂时不实施它们,因为它们很少被需要。+1
-1赞 ArunasR 10/14/2014 #7

这是一个有效的代码,但不是推荐的方式

这段代码有很多痕迹来显示它正在做什么。它不会检查分配请求的大小是否不超过缓冲区。如有必要,您可以进行此检查。请注意,std::basic_string 会尝试分配不必要的部分。

#include <string>
#include <iostream>

template<typename T, size_t S>
class fixed_allocator
{
  typedef std::allocator<T> _base;

  std::ostream& trace() const { return std::cerr << "TRACE fixed_allocator " << (void*)this ; }

public:
  typedef typename _base::value_type value_type;
  typedef typename _base::pointer pointer;
  typedef typename _base::const_pointer const_pointer;
  typedef typename _base::reference reference;
  typedef typename _base::const_reference const_reference;
  typedef typename _base::size_type size_type;
  typedef typename _base::difference_type difference_type;

  template<class C> struct rebind {
    typedef fixed_allocator<C, S*sizeof(C)/sizeof(T)> other;
  };
  T* buffer_;

  fixed_allocator(T* b) : buffer_(b) { trace() << "ctor: p="  << (void*)b << std::endl; }

  fixed_allocator() : buffer_(0) { trace() << "ctor: NULL" << std::endl; };
  fixed_allocator(const fixed_allocator &that) : buffer_(that.buffer_) { trace() << "ctor: copy " << (void*)buffer_ << " from " << (void*) &that << std::endl; };

  pointer allocate(size_type n, std::allocator<void>::const_pointer hint=0) {
    trace() << "allocating on stack " << n << " bytes" << std::endl;
    return buffer_;
  }

  bool operator==(const fixed_allocator& that) const { return that.buffer_ == buffer_; }
  void deallocate(pointer p, size_type n) {/*do nothing*/}
  size_type max_size() const throw() { return S; }
};

int main()
{
  char buffer_[256];
  fixed_allocator<char, 256> ator(buffer_);
  std::basic_string<char, std::char_traits<char>, fixed_allocator<char, 256> > str(ator);
  str.assign("ipsum lorem");
  std::cout << "String: '" << str << "' length " << str.size() << std::endl;
  std::cout << " has 'l' at " << str.find("l") << std::endl;
  str.append(" dolor sit amet");
  std::cout << "String: '" << str << "' length " << str.size() << std::endl;
  str.insert(0, "I say, ");
  std::cout << "String: '" << str << "' length " << str.size() << std::endl;
  str.insert(7, "again and again and again, ");
  std::cout << "String: '" << str << "' length " << str.size() << std::endl;
  str.append(": again and again and again, ");
  std::cout << "String: '" << str << "' length " << str.size() << std::endl;
  return 0;
}

此代码已在 GCC 和 LLVM 上进行了测试,并按预期(或意外)执行。

笨拙的语法。无法从basic_string派生并嵌入缓冲区。更好的方法是使用所需的接口子集创建一个小型专用buffer_string类basic_string。

评论

0赞 sbi 10/14/2014
您是否尝试过在第一次分配给字符串后操作字符串,以便它需要增长?
0赞 ArunasR 10/14/2014
字符串创建为空,然后进行操作(赋值)。这算不算“操纵,使其需要增长”?我没有尝试比您在代码中看到的更多的东西,但我希望basic_string简单地调用“分配”,可能带有提示,它得到的只是相同的缓冲区。
0赞 ArunasR 10/14/2014
str.append(" dolor sit amet");如果您分配足够大的缓冲区,效果非常好。basic_string试图在此分配 51 个字节,这导致原始程序出现“堆栈粉碎”异常。将缓冲区增加到 60 后,没问题。
0赞 sbi 10/14/2014
我的观点是,当字符串需要重新分配时,您只需返回指向当前数据的指针即可。但是,该字符串会将旧数据复制到新分配的内存中。是的,我忘记了,如果不更改字符串的开头,这似乎可以正常工作。但这仍然是错误的。(除非我再次遗漏了什么,否则插入会暴露这个缺陷。
0赞 ArunasR 10/14/2014
不要偷懒!剪切、粘贴、编译和播放。 效果很好。str.insert(0, "I say, ");