【问题标题】:STL queue push behaviorSTL 队列推送行为
【发布时间】:2010-10-21 03:34:42
【问题描述】:

我在 STL 队列推送中遇到了一个我不太理解的行为。

基本上,我有两个结构

structA{
  string a;
}

structB{
 char b[256];
}

structA st1;
structB st2;

...assign a 256 characters string to both st1 and st2...


queue<structA> q1;
queue<structB> q2;
for(int i=0 ; i< 10000; i++){
  q1.push(st1);
}

 for(int i=0 ; i< 10000; i++){
  q2.push(st2);
}

我意识到,与字符串结构相比,使用 char 结构的队列在推送结构时会使用更长的时间(如 5X)。在检查单个推送后,我意识到 char struct 推送性能到处都有相当多的峰值(范围从 2X 到 10X)。这是什么原因?

谢谢。

【问题讨论】:

  • 与您关于性能的其他问题一样,这严重缺乏可重复性。显示您的准确代码、您测量的准确方式以及数字。在这里提出的许多性能难题可以通过指出测试工具中的错误来解决。性能测试很难,随机发布者第一次做对的可能性很小。在你提供这些之前,我投票决定关闭它。

标签: c++ string stl


【解决方案1】:

每次将 st1 或 st2 推入队列时,实际上都是在推入它的副本(而不是引用或指针)。成本差异在于数据的复制。在structB 中,您必须每次都复制完整的 256 个字节。在structA 中,您只复制字符串实例,它很可能具有写时复制语义,因此在其中一个被修改之前,它们都将共享对底层字符串数据的相同引用。

【讨论】:

  • 写入时复制? stackoverflow.com/questions/1116040/… 显然 gcc 仍然实现了它,尽管它通常被认为是已弃用的“优化”。
  • 提出一般性问题,得到一般性答案。
【解决方案2】:

您的 C++ 实现可能使用写时复制字符串实现,这意味着字符串副本并不真正复制字符串(而是链接回副本),并且仅在您编写时“真正”复制字符串给它。

要测试是否是这种情况,请将其放入循环中,q1.push(st1) 行之后:

++st1.a[0];

然后再来一次。

显然,字符数组没有写时复制行为,并且每次您要求复制时都会“真正地”复制它。

【讨论】:

    【解决方案3】:

    字符数组比空字符串大 - 尖峰可能与必要的重新分配有关,因为向量会随着它使用的大量内存而增长。

    如果字符串不为空,那么写入时复制无论如何都会启动,因此您需要用一些锁定时间/引用计数器递增等来对抗内存使用:更快的是取决于系统。

    【讨论】:

    • 根据我的阅读,COW 在单线程场景中速度更快,没有真正的锁定争用等。这当然与 OP 的时序控制相对应——我没有看到任何线程被提及在问题中。
    • 感谢您的回复。实际上我有另一个线程正在访问队列。那么在这种情况下,它是如何工作的呢?
    • 不客气。为了安全地push() 与其他读取器线程一起使用,所有这些线程都需要使用锁(互斥锁或读/写锁)。获得该锁不会阻止 std::string 在处理复制/引用计数器时执行可能不必要的锁定,因为 std::string 中的代码不知道代码中的锁以及是否有更多线程不要使用锁,但可能会尝试从其中一个字符串中读取。不确定这是否回答了您的问题...?无论如何,使用锁进行安全编码,然后进行分析是最好的方法。
    【解决方案4】:

    原因很可能是:

    1) 动态分配内存以保存每个字符串内的字符数据
    2) 可能,但不太可能,调整支持队列的双端队列页面缓冲区的大小。

    【讨论】:

      【解决方案5】:

      std::queue 是另一个容器的适配器(实现了 front、back、push_back 和 pop_front),除非您指定要适应的容器,否则它将使用 std::deque。 Deque 在后台执行了一些块分配魔术,应该提供类似于矢量的调整大小,但性能更好,因为它管理多个不连续的块,并且每次调整大小时都不必复制所有内容。无论如何,这是一个猜测,但我会说这就是原因。

      由于为所有这些数组腾出空间,字节数组结构更频繁地看到命中,我敢打赌,在更长的范围内,字符串结构也会产生尖峰,现在它更小了,因为字符串可能维护对下属的引用字符存储,直到有东西改变它。

      现在您有机会熟悉您选择的分析器并确定找到答案!触发 valgrind (--callgrind) 或您的平台支持的任何分析器,并准确查看哪些调用正在使用时间和地点。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-07-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多