【问题标题】:Circular Buffer for Strings字符串的循环缓冲区
【发布时间】:2019-09-12 09:46:24
【问题描述】:

我正在缓冲一个进程的 stdout、stderr 和 stdin 的最后 X 行。 我想保留最后 X 行,并能够通过其 id(行号)访问一行。 因此,如果我们存储 100 行并插入其中 200 行,则可以访问第 100-200 行。 (实际上我们想要存储大约 2000 行。)

性能案例是插入。所以插入本身应该很快。检索偶尔会发生,但可能占用例的 10%。 (大部分时间我们不会查看输出。)

旧方法,碎片化
我使用了包装 ArrayDeque,然后将 book 保留在行数之上,但这意味着我们在上面的示例中使用了 [Vec<u8>;100]。一个 String 数组,因此是一个 Vec<u8> 数组。

新方法,有开放性问题
我*的新想法是将数据存储在一个 u8 数组中,然后将 book 保存在数组中每个条目的起始位置和长度上。这里的问题是我们需要簿记也是某种环形缓冲区,并在我们的数据数组必须包装的那一刻擦除旧条目。也许还有更好的方法来实现这一点?至少这充分利用了环形缓冲区并防止了内存碎片。

*也感谢 rust 社区的 sebk

目前的简单方法

const MAX: usize = 5;

pub struct LineRingBuffer {
    counter: Option<usize>,
    data: ArrayDeque<[String; MAX], Wrapping>,
    min_line: usize,
}

impl LineRingBuffer {
    pub fn new() -> Self {
        Self {
            counter: None,
            data: ArrayDeque::new(),
            min_line: 0,
        }
    }

    pub fn get<'a>(&'a self,pos: usize) -> Option<&String> {
        if let Some(max) = self.counter {
            if pos >= self.min_line && pos <= max {
                return self.data.get(pos - self.min_line);
            }
        }
        None
    }

    pub fn insert(&mut self, line: String) {
        self.data.push_back(line);
        if let Some(ref mut v) = self.counter {
            *v += 1;
            if *v - self.min_line >= MAX {
                self.min_line += 1;
            }
        } else {
            self.counter = Some(0);
        }
    }
}

被质疑的新想法草案:

pub struct SliceRingbuffer {
    counter: Option<usize>,
    min_line: usize,
    data: Box<[u8;250_000]>,
    index: ArrayDeque<Entry,Wrapping>,
}

struct Entry {
    start: usize,
    length: usize,
}

无论出于何种原因,当前的方法仍然相当快,尽管我预计会有很多不同大小的分配(取决于行)并因此产生碎片。

【问题讨论】:

    标签: data-structures rust circular-buffer


    【解决方案1】:

    让我们回到基础。

    循环缓冲区通常保证没有碎片,因为它不是按您存储的内容类型而是按大小键入的。例如,您可以定义一个 1MB 的循环缓冲区。对于固定长度类型,这为您提供了可以存储的固定数量的元素。

    你显然没有这样做。通过将Vec&lt;u8&gt; 存储为元素,即使总体数组是固定长度的,但内容却不是。存储的每个元素在数组中都是一个指向Vec(起点和长度)的胖指针。

    当然,当您插入时,您必须:

    1. 创建此Vec(这是您正在考虑但并未真正看到的碎片,因为 rust 分配器在这类事情上非常有效)
    2. 将 vec 插入应在的位置,必要时将所有内容横向移动(此处使用标准的循环缓冲区技术)

    您的第二个选项是实际循环缓冲区。如果操作正确,您将获得固定大小和零分配,但您将失去存储整行行的能力,并且 100% 保证在缓冲区的开头有整行。

    在我们进入广袤的 DYI 之前,需要快速指向VecDeque。这是您实施的更优化的版本,尽管有一些(完全保证的)unsafe 部分。


    实现我们自己的循环缓冲区

    我们将做出一系列假设并为此设定一些要求:

    • 我们希望能够存储大字符串
    • 我们的缓冲区存储字节。因此,整个堆栈都在处理拥有的u8
    • 我们将使用一个简单的Vec;实际上,您根本不会重新实现整个结构,数组纯粹是为了演示

    这些选择的结果是以下元素结构:

    | Element size | Data     |
    |--------------|----------|
    |  4 bytes     |  N bytes |
    

    因此,我们在每条消息之前丢失 4 个字节,以便能够获得对下一个元素的清晰指针/跳过引用(最大大小与 u32 相当)。

    一个幼稚的实现示例如下(playground link):

    use byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
    
    pub struct CircularBuffer {
        data: Vec<u8>,
        tail: usize,
        elements: usize,
    }
    impl CircularBuffer {
        pub fn new(max: usize) -> Self {
            CircularBuffer {
                data: Vec::with_capacity(max),
                elements: 0,
                tail: 0,
            }
        }
    
        /// Amount of elements in buffer
        pub fn elements(&self) -> usize {
            self.elements
        }
    
        /// Amount of used bytes in buffer, including metadata
        pub fn len(&self) -> usize {
            self.tail
        }
    
        /// Length of first element in ringbuffer
        pub fn next_element_len(&self) -> Option<usize> {
            self.data
                .get(0..4)
                .and_then(|mut v| v.read_u32::<NativeEndian>().ok().map(|r| r as usize))
        }
    
        /// Remove first element in ringbuffer (wrap)
        pub fn pop(&mut self) -> Option<Vec<u8>> {
            self.next_element_len().map(|chunk_size| {
                self.tail -= chunk_size + 4;
                self.elements -= 1;
                self.data
                    .splice(..(chunk_size + 4), vec![])
                    .skip(4)
                    .collect()
            })
        }
    
        pub fn get(&self, idx: usize) -> Option<&[u8]> {
            if self.elements <= idx {
                return None;
            }
            let mut current_head = 0;
            let mut current_element = 0;
            while current_head < self.len() - 4 {
                // Get the length of the next block
                let element_size = self
                    .data
                    .get(0..4)
                    .and_then(|mut v| v.read_u32::<NativeEndian>().ok().map(|r| r as usize))
                    .unwrap();
                if current_element == idx {
                    return self
                        .data
                        .get((current_head + 4)..(current_head + element_size + 4));
                }
                current_element += 1;
                current_head += 4 + element_size;
            }
            return None;
        }
    
        pub fn insert(&mut self, mut element: Vec<u8>) {
            let e_len = element.len();
    
            let capacity = self.data.capacity();
            while self.len() + e_len + 4 > capacity {
                self.pop();
            }
            self.data.write_u32::<NativeEndian>(e_len as u32).unwrap();
            self.data.append(&mut element);
            self.tail += 4 + e_len;
            self.elements += 1;
            println!("{:?}", self.data);
        }
    }
    

    请再次注意,这是一个天真的实现,旨在向您展示如何解决在缓冲区中剪切字符串的问题。 “真正的”最佳实现将 unsafe 移动和删除元素。

    【讨论】:

    • 我的基本想法与您的相同:将数据存储为 u8 的一个大 Array/Vec,您可以在其中插入不同的字符串,避免任何碎片。从您的方法中我看不到的是 a) 您如何处理访问第 n 个元素。或者 b) 遍历环形缓冲区中的所有元素。还是我理解错了? VecDeque 在我的第一个代码中使用,但它不能给我特定的行/元素。只是特定的字节。
    • 我编辑了我的问题,希望更清楚地表明我的问题是关于将第一种方法移植到连续数组结构。
    • 在我的实现中添加了get(usize);要获得迭代器,您显然需要构建一个迭代器。我强烈建议不要迭代地调用get(),而是将光标保持在您的状态。
    • 哦,谢谢。我想我现在知道了你的存储方法。这看起来很有希望。这种方法确实非常适合插入。我不明白的是,您将如何使用 rusts 自己的 vecdeque 来做到这一点。此外,如果插入不再适合,您似乎正在删除最后插入的条目?因为我想删除第一个插入,一个包装循环缓冲区。稍后必须在家里再调查一下。
    • 我在命名我的方法时搞砸了; pop 实际上是 shift,对此表示歉意。至于你将如何使用VecDeque,那是对前一种方法的评论。在存储可变长度项目时使用一个会破坏它的性能提升。
    猜你喜欢
    • 1970-01-01
    • 2011-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-10
    • 2018-12-25
    • 2015-07-20
    • 2013-09-26
    相关资源
    最近更新 更多