如何在循环缓冲区中从头到尾迭代而不分支

Posted

技术标签:

【中文标题】如何在循环缓冲区中从头到尾迭代而不分支【英文标题】:How to iterate from head to tail in circular buffer without branching 【发布时间】:2020-04-14 02:47:24 【问题描述】:

我有以下问题,尽管有很多方法都无法满足我的需求: 拥有一个索引为size_t 的循环缓冲区,而底层缓冲区可能是我喜欢的任何东西,我需要像这样向前和向后迭代: 从头到尾,从尾到头(换句话说,我需要能够遍历此缓冲区中的所有单元格)。

我有以下功能:

size_t prevIdxCircularBuffer(size_t head, size_t size, size_t capacity, size_t idx)

    const size_t sizeWithGuard = size + 1;
    const size_t prevOffset = (idx + capacity - 1 - head) % capacity;
    const size_t sizeCorrectedPrevOffset = prevOffset % sizeWithGuard;
    return (head + sizeCorrectedPrevOffset) % capacity;


size_t nextIdxCircularBuffer(size_t head, size_t size, size_t capacity, size_t idx)

    const size_t sizeWithGuard = size + 1;
    const size_t nextOffset = (idx + capacity + 1 - head) % capacity;
    const size_t sizeCorrectedNextOffset = nextOffset % sizeWithGuard;
    return (head + sizeCorrectedNextOffset) % capacity;

小说明: sizeWithGuard 是大小加尾部“守卫”元素(尾部为空)。

虽然 next 工作正常,但当 idx == head 时,previous 总是无法从头到尾迭代(它总是导致capacity - 1)。我正在寻找有关如何更改此算法以使其始终工作的线索。

对我来说至关重要的是索引必须是size_t 类型并且代码中没有分支(否则这个问题将不存在:)

【问题讨论】:

【参考方案1】:

诀窍在于有两种类型的索引:列出索引,范围从 0 到 size - 1(或者,在您的情况下,为 0 到 size 与尾部保护)和 缓冲区索引 范围从 0 到 容量 - 1。我建议稍微更改代码以澄清这一点。

size_t prevIdxCircularBuffer(size_t head, size_t size, size_t capacity, size_t idx) 
    // at the beginning, idx is a buffer index
    const size_t sizeWithGuard = size + 1;

    // listIdx is the same index, just translated to a list index
    const size_t listIdx = (idx + capacity - head) % capacity;

    // sizeCorrectedPrevOffset is the list index of the previous element
    // (ranging from 0 to size inclusively)
    const size_t sizeCorrectedPrevOffset = (prevOffset + sizeWithGuard - 1) % sizeWithGuard;

    // finally convert sizeCorrectedPrevOffset back to a buffer index
    // and return it
    return (head + sizeCorrectedPrevOffset) % capacity;

所以我改变的是:

    prevOffset 中删除了 -1 减法。将该变量重命名为 listIdx,因为它不再是“以前的”。 在 sizeCorrectedPrevOffset 中包含 -1 减法(还添加了 sizeWithGuard,因此我们保持在界限内)。

有什么区别?这是一个例子:

        tail                               head=idx
+-----------+-----------------------------+---------------+
|           |                             |               |
+-----------+-----------------------------+---------------+
 0         8                               20           36

capacity=37
head=20
idx=20
size=25+1 guard element, spanning positions [20-36] and [0-8]

我的代码版本是这样的:

listIdx = (20 + 37 - 20) % 27 = 0:正确地实现了它的列表索引为0(头部); sizeCorrectedPrevOffset = (0 + 26 - 1) % 26 = 25:新的列表索引应该是25(尾部); returns (20 + 25) % 37 = 8:尾元素的缓冲区索引

相比之下,您的代码可以:

prevOffset = (20 + 37 - 1 - 20) % 37 = 36 % 37 = 36 从这里开始偏离轨道。

【讨论】:

以上是关于如何在循环缓冲区中从头到尾迭代而不分支的主要内容,如果未能解决你的问题,请参考以下文章

循环文件映射会降低性能

Kotlin 协程Channel 通道 ② ( Channel 通道容量 | Channel 通道迭代 | 使用 iterator 迭代器进行迭代 | 使用 for in 循环进行迭代 )

Kotlin 协程Channel 通道 ② ( Channel 通道容量 | Channel 通道迭代 | 使用 iterator 迭代器进行迭代 | 使用 for in 循环进行迭代 )

C++ 中的高效循环缓冲区,将传递给 C 风格的数组函数参数

将数据读入循环缓冲区

如何在C ++中迭代void *的字节?