如何在C中实现循环缓冲区?


70

我需要一个固定大小(可在运行时在创建时选择,而不是在编译时选择)的循环缓冲区,该缓冲区可以容纳任何类型的对象,并且需要非常高性能。我认为不会出现资源争用问题,因为尽管它是在多任务嵌入式环境中,但它是一种协作的环境,因此任务本身可以管理它。

我最初的想法是在缓冲区中存储一个简单的结构,该结构将包含类型(简单的enum / define)和一个指向有效负载的空指针,但是我希望这样做尽可能快,因此我对涉及绕过的建议持开放态度堆。

实际上,我很高兴绕过任何标准库以提高原始速度-从我对代码的了解来看,它没有针对CPU进行过大幅优化:看起来他们只是为诸如此类的东西编译了C代码,strcpy()没有手工编码的程序集。

任何代码或想法将不胜感激。所需的操作是:

  • 创建具有特定大小的缓冲区。
  • 放在尾巴。
  • 从头上得到。
  • 返回计数。
  • 删除缓冲区。

1
您是否需要循环缓冲区或队列?所需的操作使其听起来像队列。我承认使用圆形缓冲区固定大小的要求是合理的,但是我不确定问题标题是否反映您的实际问题。
洛根·卡帕尔多

如果您认为其他数据结构可以更快,则可以接受,但我可以肯定地确定,内存固定循环缓冲区的性能将优于队列中的malloc / free项目。尽管我想我还是必须执行有效的malloc /释放:如果我可以为项目和有效载荷执行一个malloc,那是值得的。
paxdiablo

“如果您认为它们可以更快”?-我建议您必须进行基准测试。顺便说一句,您将什么归类为“非常高性能”?
米奇·

我将对所有想法进行基准测试(我具有基于实际吞吐量的测试数据生成功能)。“高性能”将使当前CPU能够处理客户认为合适的新增加的负载:-)
paxdiablo,2009年

我会澄清这一点。我不需要四路最新的Intel-screamer CPU的性能。它运行在不是最快的8051变体上,因此我真的只是在寻找优化想法进行测试。如果它们都不成功,客户端将不得不基于不同的CPU来制造新的硬件,这将不会便宜。当前队列中的项目处理已被识别为主要瓶颈。
paxdiablo,2009年

Answers:


10

您可以在对缓冲区进行编码时枚举所需的类型,还是需要在运行时通过动态调用添加类型?如果是前者,那么我将缓冲区创建为n个结构的堆分配数组,其中每个结构均包含两个元素:一个用于标识数据类型的枚举标记,以及一个所有数据类型的并集。就小元素的额外存储而言,您失去的是,不必处理分配/重新分配以及由此产生的内存碎片。然后,您只需要跟踪定义缓冲区的头和尾元素的开始索引和结束索引,并确保在增加/减少索引时计算mod n。


枚举所需的类型,是的。它们只有大约六个,而且永远不会改变。但是保存项目的队列必须能够保存所有六种类型。我不确定要在队列中存储整个项目而不是指针—这意味着要复制项目(几百个字节)而不是指针。但是我喜欢联合的想法-我在这里最关心的是速度而不是内存(我们已经有了足够的内存,但是CPU令人震惊:-)。
paxdiablo,2009年

您的回答给了我一个很好的主意-可以从malloc()中预先分配项目的内存,并从专门用于处理那些内存块的mymalloc()中分发出来。而且我仍然可以只使用指针。+1。
paxdiablo,2009年

您可能需要或可能不需要进行额外的复制,具体取决于数据的访问模式。如果您可以就地构建这些项目,并在它们弹出之前仍在缓冲区中时对其进行引用,则可能没有任何额外的复制。但是,将它们从您自己的分配器中分发出来并使用单独的指针数组(或索引)作为缓冲区无疑是更安全,更灵活的。
09年

81

最简单的解决方案是跟踪项目大小和项目数,然后创建一个适当字节数的缓冲区:


5
非常标准的解决方案-完全符合规范。OP包括他试图避免的内容。:P
安东尼

亚当,该解决方案假定大小相同的项目必须始终在缓冲区中占据相同的位置。我相信OP要求存储在缓冲区中任何位置的任何给定输入必须是6种大小不同的数据类型中的任何一种。剩下2个选择。为每个新到达的数据使用calloc()和realloc()空间,或者分配足够大的缓冲区以在缓冲区中的每个位置保留最大的数据类型。如果可行,后一种方法将更快,更清洁。

您能否提供一种启动循环缓冲区的方法?使用以下代码:Circular_buffer * cb; cb_init(cb,20,4); 我收到此错误,进程结束,退出代码139(被信号11:SIGSEGV打断)预先感谢您,并抱歉在注释中编写代码。
Dimitris Filippou '18年

只是想补充一点,该实现不适用于多线程情况,即生产者和使用者是单独的线程,因为两者都在访问和修改“计数”。我们将需要保护“计数”。
user138645 '20

15

只要环形缓冲区的长度是2的幂,那么令人难以置信的快速二进制“&”运算将为您缠绕索引。对于我的应用程序,我正在从麦克风获取的音频环形缓冲区中向用户显示一段音频。

我始终确保屏幕上可以显示的最大音频量远小于环形缓冲区的大小。否则,您可能正在从同一块读取和写入。这可能会给您带来奇怪的显示效果。


我喜欢使用&并使用ringBuffer-> sizeOfBuffer作为位掩码。我认为“奇怪的显示伪像”的问题是由于写入FIFO而没有在执行写入之前检查是否要在尾部进行写入?

1
我已经进行了一些测试,并且我认为%和&的方式与该代码段相同: uint8_t tmp1,tmp2; tmp1 = (34 + 1) & 31; tmp2 = (35 ) % 32; printf("%d %d",tmp1,tmp2); 那么,实际的区别是什么还是仅仅是编码风格?
R1S8K

11

首先,标题。如果使用位整数保存头和尾“指针”,并对其进行大小调整,以使它们完全同步,则不需要模运算来包装缓冲区。IE:填入12位unsigned int的4096本身就是0,无论如何都不会被破坏。消除模运算,即使对于2的幂,速度也几乎翻了一番。

在我的第三代i7 Dell XPS 8500上,使用Visual Studio 2010的C ++编译器(默认内联),进行1000万次填充和排空任何类型的数据元素的迭代需要52秒,而其中的1 / 8192nd则用于处理数据。

我会在main()中RX重写测试循环,以使它们不再控制流程-应该并且应该由指示缓冲区已满或为空的返回值控制,并伴随中断;陈述。IE:填充物和排放物应该能够相互撞击,而不会损坏或不稳定。在某些时候,我希望对该代码进行多线程处理,因此该行为至关重要。

QUEUE_DESC(队列描述符)和初始化函数强制此代码中的所有缓冲区均为2的幂。以上方案否则将无法正常工作。在主题上,请注意QUEUE_DESC并不是硬编码的,它使用清单常量(#define BITS_ELE_KNT)进行构造。(我假设此处的2的幂足够灵活)

为了使缓冲区大小运行时可以选择,我尝试了不同的方法(此处未显示),并决定将USHRT用于能够管理FIFO缓冲区的Head,Tail和EleKnt。为避免取模算术,我使用Head,Tail为&&创建了一个掩码,但是该掩码原来是(EleKnt -1),因此只需使用它即可。在安静的计算机上,使用USHRTS代替bit ints可将性能提高约15%。英特尔CPU内核始终比其总线快,因此在繁忙的共享计算机上,打包数据结构可让您在其他竞争线程之前加载并执行。权衡。

注意,缓冲区的实际存储空间是使用calloc()在堆上分配的,并且指针位于结构的基础上,因此结构和指针的地址完全相同。IE浏览器;无需将偏移量添加到结构地址即可绑定寄存器。

同样,为缓冲区提供服务的所有变量在物理上都与缓冲区相邻,并绑定到同一结构中,因此编译器可以编写漂亮的汇编语言。您必须终止内联优化才能看到任何程序集,因为否则它会被粉碎。

为了支持任何数据类型的多态性,我使用了memcpy()而不是赋值。如果您只需要灵活性来支持每个编译中的一个随机变量类型,那么此代码将完美运行。

对于多态,您只需要知道类型及其存储要求即可。描述符的DATA_DESC数组提供了一种跟踪放入QUEUE_DESC.pB​​uffer中的每个数据的方式,以便可以正确地检索它。我只分配了足够的pBuffer内存来容纳最大数据类型的所有元素,但是在DATA_DESC.dBytes中跟踪给定数据实际使用了多少存储空间。另一种方法是重新发明堆管理器。

这意味着QUEUE_DESC的UCHAR * pBuffer将具有一个并行的伴随数组来跟踪数据类型和大小,而数据在pBuffer中的存储位置将保持不变。如果您可以找到一种通过这种前向引用击败编译器提交的方法,则新成员将类似于DATA_DESC * pDataDesc或DATA_DESC DataDesc [2 ^ BITS_ELE_KNT]。在这些情况下,Calloc()总是更加灵活。

您仍然可以在Q_Put(),Q_Get中使用memcpy(),但是实际复制的字节数将由DATA_DESC.dBytes而不是QUEUE_DESC.EleBytes决定。对于任何给定的放置或获取,元素都可能具有不同的类型/大小。

我相信这段代码可以满足速度和缓冲区大小的要求,并且可以满足6种不同数据类型的要求。我以printf()语句的形式保留了许多测试装置,因此您可以(或不可以)对代码正常工作感到满意。随机数生成器演示该代码可用于任何随机的头/尾组合。


5
您的代码被破坏有几个原因。首先,比较Q->Tail == (Q->Head + Q->EleKnt)在你的Q_Put,因为方法永远不会返回trueQ->Head + Q->EleKnt不是一个模相加,这意味着你可以简单的在旁边写覆盖头部。如果EleKnt4096,那将是您Tail永远无法达到的价值。接下来,将其用作生产者/消费者队列将造成严重破坏,因为您的Q_Put“先写,以后问问题”,并Tail在意识到队列已满后更新偶数。下次调用Q_Put只会像无事发生一样覆盖头部。
Groo 2014年

2
您应该分析Wikipedia页面上介绍的用于循环缓冲区的各种算法,即完全或空缓冲区区分问题。使用当前的方法,您需要在缓冲区中少保留一个元素,以了解full和empty之间的区别。
Groo 2014年

5
@RocketRoy:我刚刚做了,Visual Studio2012。在这里,您还可以在线检查结果(stdout在底部),以便我们确定我们在看相同的代码。测试程序的输出在页面的底部,并确认了我写的内容:只要您不尝试填写它,它就会“完美地运行”。这就是为什么它不能用作FIFO缓冲区的原因。:)
Groo 2014年

5
具有讽刺意味的是,尽管您对另一个答案发表了评论,但我还是收到了这篇文章,您声称@AdamDavis的代码段无效,实际上是相反的。还请注意Put,当您仍然复制数据然后进行检查时,Adam在检测到数据已满时如何返回。
Groo 2014年

4
@RocketRoy:所以您实际上是在告诉我您仍然不同意您的代码已损坏?是的,我非常确定您的代码不会“消除差距”,因为它可以完美地工作并覆盖磁头,而无法检测出是否已满。我希望您不要在任何生命攸关的系统中使用此功能,否则可能会遇到严重的麻烦。
Groo 2014年

8

这是C语言中的一个简单解决方案。假设每个功能都关闭了中断。没有多态性和东西,只是常识。



1
应该使用pIn> pEnd代替pIn> = pEnd,否则您将永远不会填充buf的最后一个插槽;pOut> = pEnd也一样
Dmitry Kurilo

2

一个简单的实现可以包括:

  • 缓冲区,实现为大小为n的数组,无论您需要哪种类型
  • 读指针或索引(以对您的处理器更有效的方式为准)
  • 写指针或索引
  • 指示缓冲区中有多少数据的计数器(可从读取和写入指针派生,但分别跟踪速度更快)

每次写入数据时,都会前进写入指针并增加计数器。读取数据时,增加读取指针并减少计数器。如果任一指​​针到达n,请将其设置为零。

如果counter = n,则无法写入。如果counter = 0,则无法读取。


当读和写指针都指向同一位置时,如何从指针派生计数器?在这种情况下,缓冲区可能为空或已满,并且需要一个计数器(或存储缓冲区是否已满的标志)。
Dimitrios Dedoussis

@DimitriosDedoussis:这两个建议中的一个-或者您说如果指针指向同一位置,则缓冲区为空,如果写指针指向紧接在读指针之前的位置,则缓冲区已满(并且您不允许写指针前进以匹配读指针)。
史蒂夫·梅尔尼科夫

究竟。在那种情况下,缓冲区的长度必须为n + 1,以便允许容量为n。首先,我想强调的是,计数器不是从指针派生的,除非有人在其缓冲机制上进行了一些变通(例如增加缓冲区的长度)或添加了有状态的布尔标志。
Dimitrios Dedoussis

@DimitriosDedoussis是的。
史蒂夫·梅利诺科夫

2

C样式,整数的简单环形缓冲区。首先使用init而不是put和get。如果缓冲区不包含任何数据,则返回“ 0”零。


0

扩展adam-rosenfield的解决方案,我认为以下内容将适用于多线程单生产者-单消费者场景。


0

@Adam罗森菲尔德的解决方案,虽然是正确的,可以用一个更轻量级的实现circular_buffer结构不invlovecountcapacity

该结构只能容纳以下4个指针:

  • buffer:指向内存中缓冲区的开始。
  • buffer_end:指向内存中缓冲区的末尾。
  • head:指向已存储数据的末尾。
  • tail:指向存储数据的开始。

我们可以保留该sz属性以允许对存储单位进行参数化。

无论是countcapacity值应该汲取,能够使用上述指针。

容量

capacity是直截了当的,因为可以通过将buffer_end指针与buffer指针之间的距离除以存储单位来得出sz(下面的代码段是伪代码):

计数

值得一提的是,事情变得更加复杂。例如,有没有办法来确定缓冲区是否为空或满,在场景headtail指向同一个位置。

为了解决这个问题,缓冲区应该为其他元素分配内存。例如,如果循环缓冲区的期望容量为10 * sz,则需要分配11 * sz

容量公式将变为(下面的代码段是伪代码):

这种额外的元素语义使我们能够构造评估缓冲区是空还是满的条件。

空状态条件

为了使缓冲区为空,head指针指向与指针相同的位置tail

如果以上计算结果为true,则缓冲区为空。

完整状态条件

为了使缓冲区已满,head指针应在tail指针后面1个元素。因此,为了从一个head位置跳到另一个位置而需要覆盖的空间tail应等于1 * sz

如果tail大于head

如果以上计算结果为true,则缓冲区已满。

如果head大于tail

  1. buffer_end - head返回从head缓冲区跳转到缓冲区末尾的空间。
  2. tail - buffer 返回从缓冲区开始到尾部所需的空间。
  3. 将上述2相加应等于从跳转headtail
  4. 在步骤3中得出的空间,不大于 1 * sz

如果以上计算结果为true,则缓冲区已满。

在实践中

修改@Adam Rosenfield使其使用上述circular_buffer结构:

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.