如何高效的管理缓存?--LoopBuffer

我们需要一种缓存结构,可以未预知数据大小的情况下高效的管理内存。每次数据到来的时候都能保证有效的写入,即使动态的扩展内存也不会对原有的数据进行任何挪移操作。读取数据的时候只能顺序的读取,也不会对未读取到的数据进行移动。

CppNet的数据流缓冲通过CBuffer类来实现,实际的数据存储在CLoopBuffer中,loop buffer实现如其名,通过在一块固定大小的内存上移动指针来实现顺序的读写操作。

每个loop buffer都持有一块来自内存池的固定大小的内存。然后通过四个指针来严格标识数据的位置,注意这里是严格标识,所以我们申请到的内存不用memset初始化,每次读写通过移动指针来控制数据流动,下面着重说下指针的几种移动情况:

start : 指向分配内存的起始地址。
end: 指向分配内存的末端地址。
read: 当前读取游标。
write: 当前写入游标。
当loop buffer第一次被创建时,指针的位置如图1:

图1

start, read, write三个指针都指向内存的起始位置,这个时候 read = write 可读取数据为空。接下来进行数据写入,如图2:

图2

write指针开始向右移动,记录着下一次写入的位置。现在可读取的数据量是 write - read, 剩余可写入的内存大小是 end - write。

接下来 我们进行一次数据读取, 如图3:

图3

read 指针开始向右移动,读取到的数据量是 read - start,剩余可读取数据大小是 write - read,剩余可写入的内存大小是 end - write + (read - start)。

接下来我们将所有的数据读取出来, 如图4:

图4

read 指针向右移动直到追上了write,现在read == write,当读写指针相等的时候,有两种情况,要么是内存被写满,要么是内存块为空,需要一个额外的成员变量来标识。现在read追上了write,内存块为空,可读取数据大小是0,可写大小是整个内存块的大小,为了使可写缓存更为完整,以方便writev和readv的调用,每次read指针追上write指针的时候,我们都将所有指针状态重置,恢复到图1的状态。

接下来又有新的数据到来, 如图5:

图5

我们看到 write 到了read 的左边,这是因为write 一直向右移动的时候,当指向了 end 指针,则需要重新调整指向 start,这就是loop的由来,而此时read 和 start之间有不少的距离,我们接着从start开始写入数据,write又重新开始向右移动。现在可读数据大小是 end - read + (write - start), 可写数据大小是 read - write。

如果接下来还是数据写入的话, write 就会向右移动一直追上 read。这时 read == write, 但是内存已经被写满了。

为了配合readv的调用,需要有一个接口能返回当前可写内存的起始位置和大小,通过上述的几个过程我们可以观察到有两种情况:

1> 图1,图2,图5的时候(图4状态会被重置为图1),只有一个可写缓存区,起始地址是write指针,长度是read - write 或 end - write。
2> 图3的时候有两个可写区域,起始地址是write和start,可写长度分别是end - write 和 read - start。 writev时需要返回所有的数据区域,与上述情况类似但操作的指针刚好相反,不再详述。

以上的几个过程就是loop buffer写入和读取的全部情况,可以看到每次数据写入和读取的时候只有必要数据的复制,并没有对其他数据的移动拷贝操作,而且每次数据流动的时候,都不会超出限定的内存区域。

但是loop buffer只有固定大小的内存,若是写满了之后还有新的数据写入请求怎么办?这就是buffer表演的时候了。

CBuffer实现上其实和CLoopBuffer非常的相似,也是通过四个指针来控制数据的读取和写入,甚至每个指针的作用都与其相同,只不过CLoopBuffer中指针指向的内存块的具体位置,而CBuffer中的指针指向的是CLoopBuffer内存块。其内部通过一个单向链表管理所有的内存块节点,当有数据未满的时候,几个指针的移动操作和loop buffer的指针完全相同。
唯一不同的是,当所有的内存块被写满的时候,read == write,这时CBuffer需要重新从内存池中申请新的内存块,并将其添加到链表中。

以上的实现方式存在一个问题,CLoopBuffer的指针不是顺序申请的,无法通过比较指针地址来判断读写的先后顺序,所以每个CLoopBuffer在实现的时候都携带了一个自身所处队列的索引,每次查找的时候都需要重载操作符<或>的调用来判断顺序关系,valgrind性能分析时发现这里调用频次极高,所以重新优化了CBuffer的实现。

重构之后的CBuffer用一个单向链表来管理loop buffer,写入数据的时候如何空间不够,则从内存池中申请新的节点添加到链表后边,write指针向后移动。读取数据的时候,一旦当前loop buffer节点的数据全部读取完成,则将当前块归还给内存池,read指针向后移动。实现起来像是红白机游戏里的马里奥过浮桥,每次踩过的砖块都会析构掉,前边会生成新的砖块拼成浮桥。整个读写过程都是从左往右顺序移动的过程。

以上就是CppNet缓存管理的核心实现。

github请戳这里

猜你喜欢

转载自juejin.im/post/5d92e8b451882532ce31369c