【问题标题】:Would this optimization in the implementation of std::string be allowed?是否允许在 std::string 的实现中进行这种优化?
【发布时间】:2011-01-13 16:41:47
【问题描述】:

我只是在考虑std::string::substr 的实现。它返回一个新的std::string 对象,这对我来说似乎有点浪费。为什么不返回一个引用原始字符串内容并可以隐式分配给std::string 的对象?一种对实际抄袭的懒惰评价。这样的类可能看起来像这样:

template <class Ch, class Tr, class A>
class string_ref {
public:
    // not important yet, but *looks* like basic_string's for the most part

private:
    const basic_string<Ch, Tr, A> &s_;
    const size_type pos_;
    const size_type len_;    
};

这个类的公共接口将模仿真实std::string 的所有只读操作,因此使用将是无缝的。然后std::string 可以有一个新的构造函数,它接受string_ref,所以用户永远不会更聪明。当您尝试“存储”结果时,您最终会创建一个副本,因此指向数据的引用并在其背后修改它并没有真正的问题。

这个想法是这样的代码:

std::string s1 = "hello world";
std::string s2 = "world";
if(s1.substr(6) == s2) {
    std::cout << "match!" << std::endl;
}

总共构造的 std::string 对象不超过 2 个。对于执行大量字符串操作的代码,这似乎是一个有用的优化。当然,这不仅适用于std::string,还适用于任何可以返回其内容子集的类型。

据我所知,没有实现这样做。

我想问题的核心是:

给定一个可以根据需要隐式转换为std::string 的类,库编写者将成员的原型更改为返回类型是否符合标准?或者更一般地说,库编写者是否有余地在这些类型的情况下返回“代理对象”而不是常规对象作为优化?

我的直觉是这是不允许的,原型必须完全匹配。鉴于您不能仅对返回类型进行重载,那么库编写者将没有空间利用这些类型的情况。就像我说的,我认为答案是否定的,但我想我会问:-)。

【问题讨论】:

  • @Evan:您可能想阅读 The C++ Programming Language - 它提供了这个概念的实现。尽管如此,这样的 string_ref 很容易失效,并且复制通常相对便宜,因此通常人们很乐意复制到新字符串中。当我实现这样一个类时,我倾向于根据 const char* 和大小来做,这使得它更广泛地适用(例如,将它覆盖在内存映射文件、const char[] 数据等)。如果您需要独立于源来更改子字符串,那么事情很快就会变得复杂,尤其是在线程环境中......
  • @Tony: 喜欢llvm::StringRef ?
  • 当您执行 substr() 时,您不会认为 std::string 的 COW 实现尚未重用原始字符串的缓冲区。
  • @Matthieu:是的,很像 llvm::StringRef - 我敢肯定每个有经验的注重性能的 C++ 程序员都会抛出一个或三个版本...
  • @Martin York:我不知道任何适用于原始字符串子字符串的 COW 实现。这并不意味着他们不在那里,只是我似乎没有。

标签: c++ optimization standard-library proxy-object


【解决方案1】:

这个想法是copy-on-write,但不是对整个缓冲区进行 COW,而是跟踪缓冲区的哪个子集是“真正的”字符串。 (COW,以其正常形式,被(是?)用于一些库实现中。)

因此您根本不需要代理对象或更改接口,因为这些细节可以完全在内部进行。从概念上讲,您需要跟踪四件事:源缓冲区、缓冲区的引用计数以及此缓冲区中字符串的开始和结束。

任何时候操作修改缓冲区,它都会创建自己的副本(从开始和结束分隔符),将旧缓冲区的引用计数减一,并将新缓冲区的引用计数设置为一。其余的引用计数规则都是一样的:复制计数加一、析构字符串计数减一、归零并删除等。

substr 只是创建一个新的字符串实例,除非明确指定了开始和结束分隔符。

【讨论】:

  • 写时复制还是整个内存的子块?我想知道是否有任何库真正支持它(与整个字符串的写时复制相比)
  • @David:这是 COW,但要跟踪实际复制的内存子集。好问题,我想我从来没有听说过。如今,COW 本身的开销通常不值得。
  • @GMan:它需要一些地狱般的机器来跟踪所有的间隔(看看维基百科上的 IntervalTree),所以我认为实际上会更容易(实际上更快) COW 整个事情......除非允许 std::string 具有非连续布局(如 STL rope 类),在这种情况下,我猜你可以按块保持计数。
  • @Matt: 不止一个区间在哪里出现?恐怕有一个我看不到的问题迫在眉睫。
  • @GMan:当相同的基本字符串在其缓冲区的不同部分发出多个子字符串时。示例:使用std::string str = "abcdef";,我们使用ref sub1 = str.substr(0,4); // == "abcd"ref sub2 = str.substr(2,4); // == "cdef"。此时,两个不同的对象引用了重叠的间隔。
【解决方案2】:

这是一个非常有名的优化,使用相对广泛,称为写时复制或 COW。基本的事情甚至与子字符串无关,而是与

这样简单的事情有关
s1 = s2;

现在,这种优化的问题在于,对于应该在支持多线程的目标上使用的 C++ 库,必须使用原子操作访问字符串的引用计数(或者更糟的是,使用互斥锁进行保护,以防万一目标平台不提供原子操作)。这足够昂贵,以至于在大多数情况下,简单的非 COW 字符串实现更快。

参见 GOTW #43-45:

http://www.gotw.ca/gotw/043.htm

http://www.gotw.ca/gotw/044.htm

http://www.gotw.ca/gotw/045.htm

更糟糕的是,使用 COW 的库(例如 GNU C++ 库)不能简单地恢复为简单实现,因为这会破坏 ABI。 (虽然,C++0x 来拯救,因为无论如何这都需要 ABI 碰撞!:))

【讨论】:

    【解决方案3】:

    由于substr 返回std::string,因此无法返回代理对象,并且他们不能只更改返回类型或对其进行重载(出于您提到的原因)。

    他们可以通过使string 本身能够成为另一个字符串的子来做到这一点。这意味着所有使用的内存损失(保存一个额外的字符串和两个 size_types)。此外,每个操作都需要检查它是否具有字符或是否是代理。也许这可以通过一个实现指针来完成——问题是,现在我们正在让一个通用类变慢以应对可能的边缘情况。

    如果需要,最好的方法是创建另一个类substring,它从字符串、pos 和长度构造,并转换为字符串。你不能像s1.substr(6)那样使用它,但是你可以这样做

     substring sub(s1, 6);
    

    您还需要创建采用子字符串和字符串的通用操作以避免转换(因为这是重点)。

    【讨论】:

      【解决方案4】:

      关于你的具体例子,这对我有用:

      if (&s1[6] == s2) {
          std::cout << "match!" << std::endl;
      }
      

      对于通用解决方案,这可能无法回答您的问题。为此,正如@GMan 建议的那样,您需要子字符串 CoW。

      【讨论】:

        【解决方案5】:

        您所说的是(或曾经是)Java 的 java.lang.String 类 (http://fishbowl.pastiche.org/2005/04/27/the_string_memory_gotcha/) 的核心特性之一。在很多方面,Java 的 String 类和 C++ 的 basic_string 模板的设计是相似的,所以我认为使用这种“子字符串优化”编写 basic_string 模板的实现是可能的。

        您需要考虑的一件事是如何编写c_str() const 成员的实现。根据一个字符串作为另一个子字符串的位置,它可能必须创建一个新副本。如果请求 c_str 的字符串不是尾随子字符串,它肯定必须创建内部数组的新副本。我认为这需要在basic_string 实现的大多数(如果不是全部)数据成员上使用mutable 关键字,这使得其他const 方法的实现变得非常复杂,因为编译器不再能够帮助程序员具有 const 正确性。

        编辑:实际上,为了适应c_str() constdata() const,您可以使用const charT* 类型的单个可变字段。最初设置为NULL,它可以是每个实例,每当c_str() constdata() const 被调用时初始化为指向新charT 数组的指针,如果非NULL,则在basic_string 析构函数中删除.

        【讨论】:

          【解决方案6】:

          当且仅当您确实需要比 std::string 提供的性能更高的性能时,请继续编写以您需要的方式工作的东西。我以前使用过字符串的变体。

          我自己的偏好是使用非可变字符串而不是写时复制,并使用 boost::shared_ptr 或等效的但仅当字符串实际长度超过 16 时,因此字符串类也有一个私有的短字符串的缓冲区。

          这确实意味着字符串类可能会有点分量。

          我的集合列表中还有一个“切片”类,只要原始对象的生命周期完好无损,它就可以查看存在于其他地方的类的“子集”。因此,在您的情况下,我可以对字符串进行切片以查看子字符串。当然,它不会是空终止的,也没有任何方法可以在不复制它的情况下使其成为空终止。而且它不是字符串类。

          【讨论】:

            猜你喜欢
            • 2019-08-03
            • 2019-11-22
            • 1970-01-01
            • 1970-01-01
            • 2016-02-12
            • 1970-01-01
            • 2020-08-19
            • 2020-06-09
            • 1970-01-01
            相关资源
            最近更新 更多