【问题标题】:What are some behavioral anomalies with data structures that use reference counting in STL?在 STL 中使用引用计数的数据结构有哪些行为异常?
【发布时间】:2012-08-25 05:23:34
【问题描述】:

Scott Meyer 在“Effective STL”中说,在决定使用哪种数据结构时要考虑的一件事是容器是否使用引用计数。他说这种方法存在一些行为异常。

其中一些是什么?为什么像'string'和'rope'这样的容器会有异常行为?

【问题讨论】:

  • 您似乎认为该字符串必须被引用计数...
  • 如果操作计数器的关键部分不是原子的,多线程可能会成为问题。
  • 只是好奇,这是在哪一章?
  • 这样一个异常情况是,在引用计数的容器中,很难准确预测何时会产生删除某些内容的成本,而在非引用计数的容器中,您可以(更多) 轻松确定它超出范围或被显式删除的位置。
  • 被标记为民意调查/列表,但我想如果这本书没有阐明这些异常行为是什么,我会放手。

标签: c++ data-structures stl garbage-collection containers


【解决方案1】:

正如其他人所说,典型的例子是std::string。除了多线程程序中的锁定性能问题外,引用计数字符串还存在无线程问题。想象一下:

string s = "hello";
string t = s;                   // s and t share data
char &c = t[0];                 // copy made here, since t is non-const

问题是非常量 operator[] 必须复制字符串,如果它是共享的,因为返回的引用可以稍后用于修改字符串(您可以将其分配给非引用 char ,但operator[] 不知道它的行为应该有什么不同)。另一方面,const operator[] 应该避免复制,因为这会消除引用计数的所有好处(这意味着您在实践中总是复制) .

const char &get_first(const string &s) {
    return s[0];                // no copy, s is const
}

string s = "hello";
string t = s;                   // s and t share data
const char &c1 = get_first(t);  // no copy made here
const char &c2 = t[0];          // copy made, since t is non-const
// c1 just got invalidated (in fact, it's pointing at s[0], not t[0]).
s[0] = 'X';
printf("%c, %c\n", c1, c2);     // outputs "X, h"

如您所见,这种区别令人困惑,并可能导致真正意外的行为。

这是一篇关于写时复制语义及其对性能影响的旧文章:http://www.gotw.ca/gotw/045.htm

以下是更改 std::string 以使其在 C++11 标准中不被引用计数的提议:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2534.html。这就是上面例子的基础。

【讨论】:

  • 一个模糊问题的优秀答案:)
【解决方案2】:

作为一个示例,引用计数字符串可能发生的一个异常,特别是具有“子部分”处理(带有开始/结束切片)的字符串,是“不幸的锁定”。

假设您为文件的整个文本分配内存。然后你解析你的文件并使用一些“slice()”、“left()”、“mid()”或等效方法。您可能会结束锁定文件的整个字符串,而其中可能只有很小的一部分包含实际的文本数据(其余部分是已经解析的数字、标点符号或其他)。因此,您最终可能使用了更多的内存,同时更容易控制峰值使用量。在这种情况下,如果您使用多线程并在各个线程中大量使用某些字符串,则可能会出现第二个问题:不必要的内存争用,字符串的引用计数可能会增加/减少所有时间和原子性可能会妨碍,减慢所有线程的完成速度。

不过,只要您知道应用程序中的潜在问题并阻止它们(在这种情况下,只需通过复制字符串来“单独”制作),就没有什么反对引用计数的。

【讨论】:

    【解决方案3】:

    作为一般规则,引用计数通常会遇到竞争条件、死锁或过度同步等常见的多线程问题,以避免任何一种情况。

    然后你会遇到上下文问题,这通常需要类似闭包的行为,即对象可能需要在它们明显超出范围后被捕获,但这可以通过 STL 避免,我不是 STL 专家。

    这里有一个讨论讨论与智能指针相关的各种巴洛克式边缘情况:http://www.velocityreviews.com/forums/t689414-c-primer-4th-edition-reference-counting-smart-pointers.html

    【讨论】:

      【解决方案4】:

      在第 13 项中,Meyers 详细阐述了多线程和引用字符串的问题。

      这在很大程度上取决于std::string 和锁定的确切实现以及使用模式。
      如果在多线程环境中明显无害地使用 std::string 会因隐藏锁而导致延迟,可能会出现问题。循环中这种锁和上下文切换的代价可能是巨大的。但它绝不应该导致死锁。
      这不一定是个问题。这本书是> 10岁。与此同时,线程实现也得到了改进。 Linux-Futex,例如在大多数情况下表现得更平滑。

      另一点:(我,不知道迈耶斯是否也讨论过这个......)
      引用 std::string 意味着它具有写时复制语义。这通常是一件好事。实际副本被推迟到实际需要时。但这也意味着必须在可能难以预测的时候支付副本的价格。

      【讨论】:

      • 多核处理器的锁定并没有变得更便宜。锁必然需要全局可见的写入。而 COW 的问题在于,当你无法确定是否需要锁时,往往需要付出加锁的代价,需要谨慎行事。
      • 在现代实现中没有锁定变得更便宜。获取一个空闲锁并释放一个没有人等待的锁几乎不需要任何成本。只有其他 2 个案例仍然很贵。
      猜你喜欢
      • 2011-05-24
      • 1970-01-01
      • 2023-03-22
      • 2013-03-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-08-06
      相关资源
      最近更新 更多