我应该使用哪个STL容器作为FIFO?


89

哪个STL容器最适合我的需求?我基本上有一个10个元素宽的容器,在其中我会不断添加push_back新元素,同时pop_front选择最旧的元素(大约一百万次)。

我目前正在使用a std::deque来执行任务,但想知道a std::list是否会更高效,因为我不需要重新分配自身(或者我误以为a std::dequestd::vector)。还是有一个我需要的效率更高的容器?

PS我不需要随机访问


5
为什么不尝试同时使用它和时间来查看哪个速度更快满足您的需求呢?
KTC

5
我正要这样做,但我也在寻找理论上的答案。
加布·罗耶

2
std::deque不会重新分配。它是a std::list和a 的混合体std::vector,它分配的块比a大,std::list但不会像a那样重新分配std::vector
马特·普赖斯

2
不,这是该标准的相关保证:“在双端队列的开始或结尾处插入单个元素总是需要固定的时间,并导致对T的复制构造函数的单个调用。”
马特·普赖斯

1
@John:不,它再次分配。也许我们只是在混淆术语。我认为重新分配意味着采用旧分配,将其复制到新分配中,并丢弃旧分配。
GManNickG 2009年

Answers:


194

由于有无数的答案,您可能会感到困惑,但总结一下:

使用std::queue。原因很简单:它是一个FIFO结构。您需要FIFO,请使用std::queue

它使您的意图对其他任何人,甚至您自己都清晰可见。一个std::liststd::deque不。列表可以在任何地方插入和删除,这不是FIFO结构应该执行的操作,列表可以deque在任一端进行添加和删除,这也是FIFO结构不能执行的操作。

这就是为什么您应该使用的原因queue

现在,您询问了性能。首先,请始终记住以下重要经验法则:好的代码优先,性能的最后。

原因很简单:在干净和优雅之前追求性能的人们几乎总是最后完成。他们的代码变得扑朔迷离,因为他们已经放弃了所有好的东西,以便从中真正受益匪浅。

通过首先编写良好的可读性代码,您中的大多数性能问题都可以解决。而且,如果以后发现您的性能不佳,现在可以轻松地将分析器添加到漂亮,干净的代码中,并找出问题出在哪里。

综上所述,这std::queue只是一个适配器。它提供了安全的接口,但内部使用了不同的容器。您可以选择此基础容器,这样可以提供很大的灵活性。

那么,您应该使用哪个底层容器?我们知道,std::list并且std::deque都提供必要的功能(push_back()pop_front(),和front()),所以我们如何决定?

首先,要了解分配(和取消分配)内存通常不是一件容易的事,因为这涉及到操作系统并要求其做某事。A list必须在每次添加某项内容时分配一次内存,并在内存消失后对其进行分配。

deque另一方面,A分块分配。它分配的频率要低于list。可以将其视为一个列表,但是每个内存块可以容纳多个节点。(当然,我建议您真正了解它的工作原理。)

因此,仅凭这一项deque,它就应该会表现更好,因为它不会经常处理内存。加上您正在处理恒定大小的数据这一事实,它可能在第一次传递数据后就不必分配,而列表将不断地分配和释放。

要了解的第二件事是缓存性能。进入RAM的速度很慢,因此当CPU确实需要时,通过将一部分内存带回缓存来充分利用这段时间。由于a deque在内存块中进行分配,因此访问此容器中的元素很可能会导致CPU也带回容器的其余部分。现在,对的任何进一步访问deque都会很快,因为数据在缓存中。

这与列表不同,在列表中一次分配一个数据。这意味着数据可能会散布到内存中的所有位置,并且缓存性能会很差。

因此,考虑到这一点,a deque应该是一个更好的选择。这就是使用时默认容器的原因queue。综上所述,这仍然只是(非常)有根据的猜测:您必须使用deque一个测试和另一个测试来概要分析此代码,list才能真正确定。

但是请记住:让代码使用干净的接口,然后担心性能。

John提出了一个问题,即将list或换行deque会导致性能下降。再一次,他或我也可以肯定地说而不自己分析,但是编译器可能会内联make的调用queue。也就是说,当您说时queue.push(),它实际上只是说queue.container.push_back(),完全跳过了函数调用。

再一次,这只是一个有根据的猜测,但queue与使用基础容器raw相比,使用a 不会降低性能。就像我之前说过的那样,请使用queue,因为它很干净,易于使用且安全,并且确实会成为问题档案和测试。


10
+1-如果事实证明boost :: circular_buffer <>具有最佳性能,则可以将其用作基础容器(它还提供所需的push_back(),pop_front(),front()和back() )。
Michael Burr,2009年

2
接受详细解释(这是我需要的,感谢您抽出宝贵的时间)。至于最后的良好代码优先性能,我必须承认这是我最大的默认值之一,我总是在首次运行时就尽力做到完美……我确实是使用双端队列来编写代码的,但事实并非如此。如果我的表现不如我所想(应该是实时的),我想我应该对此加以改进。正如Neil所说,我确实应该使用探查器...虽然我很高兴现在犯了这些错误,但并不重要。非常感谢大家。
加布·罗耶

4
-1为未解决问题而肿无用的答案。正确的答案很简短,它是boost :: circular_buffer <>。
Dmitry Chichkov 2014年

1
“好的代码优先,性能的最后”,这是一个很棒的报价。如果只有每个人都可以理解:)
thegreendroid

我感谢剖析带来的压力。提供经验法则是一回事,然后通过概要分析来证明是一件好事
talekeDskobeDa

28

结帐std::queue。它包装了基础容器类型,默认容器为std::deque


3
编译器消除每个额外的层。按照您的逻辑,我们所有人都应该只用汇编语言编写程序,因为该语言只是一个妨碍您运行的外壳。关键是要为作业使用正确的类型。而且queue是那种类型。好的代码首先,性能更高。糟糕,大多数性能首先来自使用好的代码。
GManNickG 2009年

2
抱歉,含糊不清-我的意思是,队列正是问题所在,C ++设计人员认为deque是此用例的良好基础容器。
Mark Ransom

2
这个问题没有任何迹象表明他发现自己缺乏表现。许多初学者一直在寻求针对任何给定问题的最有效解决方案,而不管其当前解决方案的性能是否令人满意。
jalf

1
@John,如果他发现缺乏queue性能,那么像我刚才所说的那样,剥去安全壳将无法提高性能。您建议使用list,可能会导致效果更差。
GManNickG 2009年

3
关于std :: queue <>的事情是,如果deque <>不是您想要的(出于性能或出于某种原因),则将其更改为使用std :: list作为后备存储是一种唯一的选择- GMan说回来了。而且,如果您确实要使用环形缓冲区而不是列表,则boost :: circular_buffer <>将直接插入... std :: queue <>几乎绝对是应使用的“接口”。它的后备存储可以随意更改。
迈克尔·伯


7

我在选择最旧的push_back元素的同时不断添加新元素pop_front(大约一百万次)。

在计算中,一百万确实不是很大的数字。正如其他人建议的那样,请使用a std::queue作为您的第一个解决方案。在不太可能的情况下,如果它太慢,请使用探查器确定瓶颈(不要猜测!),并使用具有相同接口的其他容器重新实现。


1
好吧,这是一个很大的数目,因为我想做的事应该是实时的。尽管您是对的,但我应该使用探查器来查明原因……
Gab Royer

关键是,我不太习惯使用探查器(我们在其中一个类中曾使用过gprof,但实际上并没有深入探讨……)。如果您能指出一些资源,我将不胜感激!PS。我使用VS2008
Gab Royer

@Gab:您拥有哪个VS2008(Express,Pro ...)?有些带有分析器。
sbi

@Gab对不起,我不使用任何VS多所以不能真心奉劝

@Sbi,据我所知,它仅在团队系统版本(我可以访问)中。我会研究这个。
加布·罗耶

5

为什么不std::queue呢?它所拥有的就是push_backpop_front


3

一个队列可能是一个比一个更简单的接口,双端队列,但对于这样的小单子,在性能上的差异可能是微不足道的。

同去的名单。取决于您想要的API的选择。


但我想知道,持续不断的push_back是造成队列还是让双端队列重新分配自己
Gab Royer

std :: queue是另一个容器的包装器,因此包装双端队列的队列比原始双端队列的效率低。
约翰·米利金

1
对于10个项目,性能很可能会成为一个很小的问题,以致于在程序员时间比代码时间更好地衡量“效率”。通过任何不错的编译器优化,从队列到双端队列的调用都将变为零。
lavinio

2
@John:我希望您向我展示一组基准,以证明这种性能差异。它的效率不低于原始双端队列。C ++编译器非常积极地内联。
jalf

3
我已经试过了 :DA快速且肮脏的10元素容器,在VC9上发布版本以提高速度时,具有100,000,000 pop_front()和push_back()rand()int编号可以提供:list(27),queue(6),deque(6),array(8) 。
KTC

0

使用std::queue,但是要知道两个标准Container类的性能折衷。

默认情况下,std::queue是上的适配器std::deque。通常,在队列数量少且包含大量条目的队列中,这样做会带来良好的性能,这在通常情况下是可以肯定的。

但是,不要盲目实现std :: deque。特别:

“ ...双端队列通常具有很大的最小内存开销;仅包含一个元素的双端队列必须分配其完整的内部数组(例如,64位libstdc ++的对象大小的8倍;对象大小的16倍或4096字节,以较大者为准) ,在64位libc ++上)。”

为了消除这种情况,假设队列条目是您要排队的东西,即大小相当小,那么如果您有4个队列,每个队列包含30,000个条目,std::deque则将选择实现。相反,如果您有30,000个队列,每个队列包含4个条目,则std::list实现最佳方案的可能性将更大,因为您将永远不会分摊std::deque这种情况下的开销。

您会读到很多关于缓存如何为王,Stroustrup如何讨厌链表等的观点,在某些情况下,所有这些都是正确的。只是不要盲目地接受它,因为在第二种情况下,默认std::deque实现不太可能执行。评估您的用法和度量。


-1

这种情况很简单,您可以自己编写。对于使用STL占用太多空间的微型控制器而言,这是一种很好的方法。这是将数据和信号从中断处理程序传递到主循环的好方法。

// FIFO with circular buffer
#define fifo_size 4

class Fifo {
  uint8_t buff[fifo_size];
  int writePtr = 0;
  int readPtr = 0;
  
public:  
  void put(uint8_t val) {
    buff[writePtr%fifo_size] = val;
    writePtr++;
  }
  uint8_t get() {
    uint8_t val = NULL;
    if(readPtr < writePtr) {
      val = buff[readPtr%fifo_size];
      readPtr++;
      
      // reset pointers to avoid overflow
      if(readPtr > fifo_size) {
        writePtr = writePtr%fifo_size;
        readPtr = readPtr%fifo_size;
      }
    }
    return val;
  }
  int count() { return (writePtr - readPtr);}
};

但是,这将如何/何时发生?
user10658782

哦,出于某些原因,我认为可以。没关系!
Ry-
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.