本文共 14341 字,大约阅读时间需要 47 分钟。
写在前面
本篇是CACHE压缩技术的第五篇,解读的论文是
Base-Delta-Immediate Compression: Practical Data Compression for
On-Chip Caches
作者是来自CMU和INTEL的实验室的研究人员
其他Cache压缩、Cache原理的文章链接如下:
FPC压缩论文解读
FLIP-N-WRITE详解
数据压缩学习(一)
数据压缩学习(三)
背景知识
Cache压缩技术是一种很有前途的提高片内Cache容量、降低片内和片外带宽利用率的技术。不幸的是,直接应用众所周知的压缩算法(通常在软件中实现)会导致高硬件复杂性和不可接受的解压缩/压缩特性,进而会对性能产生负面影响。因此,需要一种简单而高效的压缩技术,能够有效地压缩缓存中常见的数据模式,并且对缓存访问延迟的影响最小。
缓存压缩是在缓存填充后(在提供关键字之后)在后台进行的,
而缓存解压缩是在缓存命中的关键路径上进行的,其中最小化延迟对性能极其重要。
事实上,由于一级缓存命中时间是最重要的,因此在本研究中,我们只考虑二级缓存的压缩
三个核心目标
同时解决压缩比小、硬件复杂度高、解压缩延迟大的问题。
数据压缩的几种模式
实际应用程序访问的数据中存在大量冗余。有多种模式导致了这种冗余。我们在下面总结了这些模式中最常见的一些:
零模式
零是应用数据中最常见的值。原因是多方面的。例如,零最常用于初始化数据、表示空指针或假布尔值以及表示稀疏矩阵(密集形式)。
重复模式
一个大的连续内存区域可能包含一个重复多次的值[23]。这种模式广泛存在于对大数组使用公共初始值的应用程序中,或者在相邻像素的大量数目具有相同颜色的多媒体应用程序中
窄模式
一句话就是习惯性把count设成double/long类型结果上限就是10,浪费极大
窄值是使用大数据类型存储的小值:例如,作为四字节整数存储的一个字节值。由于过度配置或数据对齐,应用程序数据中通常会出现窄值。程序员通常会在最坏的情况下预测各种数据结构中的数据类型,即使大多数值可能适合较小的数据类型。例如,存储计数器表需要设置数据类型以容纳计数器的最大可能值。然而,可能的情况是最大可能的计数器值需要4个字节,而一个字节可能足以存储大多数计数器值。
其他模式
还有一些其他常见的数据模式不属于上述三类中的任何一类:指向同一内存区域中不同位置的指针表、低颜色梯度变化的图像,等。
下图给出了上述问题方案的一些对比
核心思想
关键思想是,对于许多缓存线,缓存线中的值具有较低的动态范围,即,缓存线中存储的值之间的差异很小。因此,可以使用一个基值和一个差异数组来表示缓存线,这些差异数组的组合大小远小于原始缓存线(我们称之为基+增量编码)
于我们研究的工作负载,最好的选择是有两个基,其中一个基总是零。
使用这两个基值(零和其他值),我们的方案可以有效地压缩包含两个分离率动态范围的混合缓存线:一个以从缓存线的实际内容中选择的任意值(例如指针值)为中心,另一个接近于零(例如小整数值)。
最大的区别是:
以往的方法偏重于充分利用单个模式进行压缩,而bdi偏重于一个简单而高效的方法。(说实话这我是真没理解这句话的)
our goal is to exploit the general case of values with low dynamic range to build a simple yet effective compres-sion technique.
我觉得这么理解可能更好一点:以往的方法都是在一个cache line里面进行压缩的工作,粒度是单个字是否压缩,而bdi只有两种情况:1.整个cache line都压缩 。 2.整个cache line都不压缩。
因此,可以使用比表示字本身所需的字节更少的字节来表示这样一个缓存线中单词之间的差异。我们利用这一观察结果,使用公共基和delta数组(缓存线和公共基中的值之间的差异)来表示动态范围较低的缓存线。由于增量所需的字节数少于值本身,因此基增量和增量数组的组合大小可能比原始未压缩缓存线的大小小得多。
前提假设
我们假设这样一种设计:压缩方案可以为压缩缓存线存储比未压缩基线缓存中存储的缓存线数量多两倍的标记。
对比的对象
频繁值压缩(FVC)
频繁模式压缩(FPC)
BDI实例
图3和图4显示了应用程序h264ref和perlbench中的两条32字节4缓存线的压缩。h264ref的第一个示例显示了一个缓存线,其中有一组存储为4字节整数的窄值。如图3所示,在这种情况下,可以使用单个4字节的基值0和8个1字节的差异数组来表示缓存线。结果,整个缓存线数据可以用12个字节而不是32个字节来表示,节省了20个字节的原始使用空间。图4显示了一个类似的现象,附近的指针存储在perlbench应用程序的同一缓存行中。
图三显示的是典型的窄字模式,每个接近0的数字都被存储为4B的数据,显然是一种浪费,通过选0x00000000为base,其他的只用1B存增量就可以很好的节约空间。
图四显示了典型的指针分配连续存储的情况
压缩算法详解
一些约定
每个cache line是 C Bytes
每个被压缩的集合都是k Bytes
对于64B的cache line而言,有88B,164B,32*2B三种规格
目标是确定B*(BASE值)和k的值,{k,B*,delta = (delta1,delta2…)}
几个观察
要使缓存线可压缩,表示差异所需的字节数必须严格小于表示其自身值所需的字节数。也就是说原本需要8个字节表示,你要是用16个字节的增量来表示了,那不是搞笑吗
B值的确定和cache line的关系
B的最佳值应介于min(S)和max(S)之间。事实上,只有在最小值、最大值或介于两者之间时才能达到最佳值。
参数的确定
确定k的值
为所有缓存线选择一个k值将显著减少压缩的机会,举个例子:考虑两条缓存线,一条表示指向某个内存区域的4字节指针表(类似于图4),另一条表示存储为2字节整数的窄值数组。对于第一个缓存线,k的可能最佳值是4,因为将缓存线划分为一组具有不同k的值可能会导致动态范围的增加并降低压缩的可能性。类似地,对于第二个缓存线,k的最佳值可能是2。
因此,为了通过迎合多种模式来增加压缩的机会,我们的压缩算法尝试同时使用三个不同的k值:2、4和8来压缩缓存线。然后使用提供最大压缩率或根本不压缩的值压缩缓存线
确定B∗。
对于k∈{2,4,8}的每个可能值,缓存线被分割成大小为k的值,并且对于基的最佳值B可以使用观察2来确定。然而,以这种方式计算B∗需要计算值集的最大值或最小值,这增加了逻辑复杂性并显著增加了压缩的延迟。
为了避免压缩延迟增加和降低硬件复杂性,我们决定使用值集的第一个值作为B的近似值。
解压过程
vi简单地由vi=B∗+Δi给出。因此,可以使用SIMD式矢量加法器并行计算缓存线中的值。因此,使用一组简单的加法器,可以在进行整数向量加法所需的时间内对整个缓存线进行解压缩。
使用多个基的原因
很明显,并不是每个缓存线都可以用这种形式表示,因此,一些基准没有高压缩比,例如mcf。发生这种情况的一个常见原因是,其中一些应用程序可以在同一缓存线中混合不同类型的数据,例如指针和1字节整数的结构。
下图显示了原因:
图显示了来自mcf的一条32字节缓存线,很明显,如果我们使用两个基,这个缓存线可以很容易地使用类似的压缩技术来压缩,就像在B+的算法中使用一个基一样。因此,可以使用19个字节来表示整个缓存线数据:8个字节表示两个基(0x00000000和0x09A40178),5个字节表示第一个基的5个1字节增量,6个字节表示第二个基的3个2字节增量。这有效地节省了32字节行中的13字节。
经过实验,发现两个基压缩是最好的。不幸的是,有两个基的B+有一个严重的缺点:必须找到第二个基。搜索第二个arbi trary基值(甚至是次优值)可以为压缩硬件增加重要的复杂度。这就打开了如何有效地找到两个基本值的问题。接下来,我们提出了一种以最小的复杂度实现双基压缩的机制。
大多数情况下,当不同类型的数据混合在同一缓存线中时,原因是聚合数据类型:例如,结构(C中的结构)。在许多情况下,这会导致宽值与低动态范围(例如指针)与窄值(例如小整数)的混合。第一个任意基有助于使用基+增量编码压缩动态范围较低的宽值,而第二个零基足够有效地将窄值与宽值分开压缩。基于这一观察,我们通过添加一个额外的隐式基(al ways设置为零)来改进
BDI的硬件实现
压缩单元的设计
并行的接受8个压缩单元的压缩,判断是否可以压缩和压缩后的cache line的值,然后选择一个最短的送给CCL
图9描述了32字节缓存线的8字节基1字节D压缩单元的组织。压缩器将该缓存线“视”为一组4个8字节元素(V0、V1、V2、V3),在第一步中,计算基本元素和所有其他元素之间的差异。回想一下,基(B0)被设置为第一个值(V0),正如我们在第3节中所描述的。然后检查得到的差异值,看它们的前7个字节是全零还是全一(1字节符号扩展检查)。如果是这样,则结果缓存线可以存储为基值B0和差分D0、D1、D2、D3的集合,其中每个D只需要一个字节。在这种情况下,压缩的缓存线大小是12字节,而不是原来的32字节。如果1字节符号扩展检查false返回(即至少有一个di不能用1字节表示),则压缩单元无法压缩此缓存线。
解压器的实现,一个并行加法器而已
缓存组织的设计
左边是传统2路组相联的cache,右边是BDI的cache设计。
关于传统的cache设计,可以参考我另外一篇文章。
缓存压缩可能允许在同一个数据存储中存储比传统的非压缩缓存更多的缓存线。
但是,为了访问这些额外的压缩缓存线,我们需要一种方法来处理它们。实现这一点的一种方法是拥有比我们在相同大小和关联性的传统高速缓存中拥有的数目更多的标记,例如两倍多。然后,我们可以使用这些附加标记作为指向相应数据存储中更多数据元素的指针。
原来的一个tag对应了一个数据块,现在一个标记分为前半部分C代表压缩模式位,后半部分为原标记。
图11显示了缓存设计中所需的更改。具有32字节缓存线(如上图所示)的传统双向缓存有一个标记存储,每组两个标记,以及一个数据存储,每组两个32字节缓存线。每个标记都直接映射到数据存储的相应部分。在BDI设计中,我们有两倍多的tag(本例中为四个),每个标记还有4个额外的位来表示行是否被压缩,如果是,则使用什么压缩类型(见表2中的“编码”)。数据存储的大小与以前一样(2×32=64字节),但它被分成更小的固定大小的段(如图11中的8字节)。每个标记存储起始段(例如,T ag2存储段S2)和缓存块的编码。通过知道编码,我们可以很容易地知道缓存块使用的段数。
BDI编码表
实现中的一些点
我们建议在高于L1(例如L2和L3)的缓存级别使用我们的B∏I设计。虽然可以压缩一级缓存中的数据,但这样做会增加延迟敏感的一级缓存命中的关键路径。这可能会导致不受益于压缩的应用程序的性能显著下降。
现在,我们将描述BDI缓存如何与使用BDI压缩的二级缓存层次结构(L1、L2和主内存)的系统相适应-请注意,唯一的更改是二级缓存。我们假设所有缓存都使用写回策略。与压缩二级缓存操作相关的场景有四种:
1)二级缓存命中,
2)二级缓存未命中,
3)一级缓存到二级缓存的写回,以及
4)二级缓存到内存的写回。
首先,在二级缓存命中时,将相应的缓存线发送到一级缓存。如果该行被压缩,则在将其发送到一级缓存之前首先将其解压缩。其次,在二级缓存未命中时,相应的缓存线从内存中取出并发送到一级缓存。在这种情况下,行也被压缩并插入到二级缓存中。第三,当一行从L1写到L2时,它首先被压缩。如果行的旧副本已存在于二级缓存中,则旧(旧)副本将失效。然后将新的压缩缓存线插入二级缓存。第四,当一行从二级缓存写回内存时,它在被发送到内存控制器之前被解压缩。在第二和第三种情况下,可能会根据第5.1节中描述的缓存逐出策略从二级缓存逐出多个缓存线。
背景
由于内核文件系统引入page cache机制,通常的写操作被延迟写入磁盘,当内存中的page cache数据被用户写了但是还没有刷入到磁盘设备,则page cache被标识为脏页dirty,脏页会在下面的几种情况下刷入磁盘:
脏页时间超过了某个阈值
脏页比例超过了某个阈值 内存紧张申请得不到满足 用户系统调用sync之类 在内核2.6.1x版本使用的是pdflush机制,因为管理了所有的磁盘设备所以存在严重的IO性能瓶颈,所以在2.6.3x开始脏页回写由bdi_wirteback机制负责,bdi_wirteback为每个磁盘创建一个bdi和对应线程,专门复制磁盘的刷入工作提高IO性能。pdflush
pdflush是2.6.1x版本之前采用的机制,由于没有看过代码所以暂不分析。
BDI
BDI是backing device info的缩写,它用于描述后端存储(如磁盘)设备相关的信息。相对于内存来说,后端存储的I/O比较慢,因此写盘操作需要通过page cache进行缓存延迟写入。
bdi-default
最初的BDI子系统里,内核版本2.6.3x,模块启动的时候创建bdi-default进程,然后为每个注册的设备创建flush-x:y(x,y为主次设备号)的进程,用于脏数据的回写。由于没有看过代码所以暂不分析。
workqueue
在Linux 3.10.0版本之后,BDI子系统使用workqueue机制代替原来的线程创建,需要回写时,将flush任务提交给workqueue,最终由通用的[kworker]进程负责处理。
BDI子系统初始化的代码如下:
static int __init default_bdi_init(void){
int err;
bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE |
WQ_UNBOUND | WQ_SYSFS, 0);
if (!bdi_wq)
return -ENOMEM;
err = bdi_init(&default_backing_dev_info);
if (!err)
bdi_register(&default_backing_dev_info, NULL, "default");
err = bdi_init(&noop_backing_dev_info);
return err;
}
subsys_initcall(default_bdi_init);
mount ext4文件系统时,初始化设置默认的default_backing_dev_info,但是在哪儿注册的呢?
static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags,const char *dev_name, void *data)
{
return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
}
struct dentry *mount_bdev(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data,
int (*fill_super)(struct super_block *, void *, int))
{
struct block_device *bdev;
struct super_block *s;
s = sget(fs_type, test_bdev_super, set_bdev_super, flags | MS_NOSEC,
bdev);
s = alloc_super(type, flags);
s->s_bdi = &default_backing_dev_info;
BDI子系统使用workqueue机制进行数据回写,其回写接口为bdi_queue_work()将具体某个bdi的回写请求(wb_writeback_work)挂到bdi_wq上。
static void bdi_queue_work(struct backing_dev_info *bdi,struct wb_writeback_work *work)
{
trace_writeback_queue(bdi, work);
spin_lock_bh(&bdi->wb_lock);
if (!test_bit(BDI_REGISTERED, &bdi->state)) {
if (work->done)
complete(work->done);
goto out_unlock;
}
list_add_tail(&work->list, &bdi->work_list);
mod_delayed_work(bdi_wq, &bdi->wb.dwork, 0);
out_unlock:
spin_unlock_bh(&bdi->wb_lock);
}
然后调用wait_for_completion(&done);阻塞等待请求被取走。sync_inodes_sb同步函数会调用到这里。 void sync_inodes_sb(struct super_block *sb){
DECLARE_COMPLETION_ONSTACK(done);
struct wb_writeback_work work = {
.sb = sb,
.sync_mode = WB_SYNC_ALL,
.nr_pages = LONG_MAX,
.range_cyclic = 0,
.done = &done,
.reason = WB_REASON_SYNC,
};
/* Nothing to do? */
if (sb->s_bdi == &noop_backing_dev_info)
return;
WARN_ON(!rwsem_is_locked(&sb->s_umount));
bdi_queue_work(sb->s_bdi, &work);
wait_for_completion(&done);
wait_sb_inodes(sb);
}
EXPORT_SYMBOL(sync_inodes_sb);
bdi_queue_work()提交了work给bdi_wq上,由对应的bdi处理函数进行处理,默认的函数为bdi_writeback_workfn。
void bdi_writeback_workfn(struct work_struct *work){
struct bdi_writeback *wb = container_of(to_delayed_work(work),
struct bdi_writeback, dwork);
struct backing_dev_info *bdi = wb->bdi;
long pages_written;
set_worker_desc("flush-%s", dev_name(bdi->dev));
current->flags |= PF_SWAPWRITE;
if (likely(!current_is_workqueue_rescuer() ||
!test_bit(BDI_REGISTERED, &bdi->state))) {
/*
* The normal path. Keep writing back @bdi until its
* work_list is empty. Note that this path is also taken
* if @bdi is shutting down even when we're running off the
* rescuer as work_list needs to be drained.
*/
do {
pages_written = wb_do_writeback(wb, 0);
trace_writeback_pages_written(pages_written);
} while (!list_empty(&bdi->work_list));
} else {
/*
* bdi_wq can't get enough workers and we're running off
* the emergency worker. Don't hog it. Hopefully, 1024 is
* enough for efficient IO.
*/
pages_written = writeback_inodes_wb(&bdi->wb, 1024,
WB_REASON_FORKER_THREAD);
trace_writeback_pages_written(pages_written);
}
if (!list_empty(&bdi->work_list) ||
(wb_has_dirty_io(wb) && dirty_writeback_interval))
queue_delayed_work(bdi_wq, &wb->dwork,
msecs_to_jiffies(dirty_writeback_interval * 10));
current->flags &= ~PF_SWAPWRITE;
}
首先判断当前workqueue能否获得足够的worker进行处理,如果能则将bdi上所有work全部提交,否则只提交一个work并限制写入1024个pages。正常情况下通过调用wb_do_writeback函数处理回写。 long wb_do_writeback(struct bdi_writeback *wb, int force_wait){
struct backing_dev_info *bdi = wb->bdi;
struct wb_writeback_work *work;
long wrote = 0;
set_bit(BDI_writeback_running, &wb->bdi->state);
while ((work = get_next_work_item(bdi)) != NULL) {
/*
* Override sync mode, in case we must wait for completion
* because this thread is exiting now.
*/
if (force_wait)
work->sync_mode = WB_SYNC_ALL;
trace_writeback_exec(bdi, work);
wrote += wb_writeback(wb, work);
/*
* Notify the caller of completion if this is a synchronous
* work item, otherwise just free it.
*/
if (work->done)
complete(work->done);
else
kfree(work);
}
/*
* Check for periodic writeback, kupdated() style
*/
wrote += wb_check_old_data_flush(wb);
wrote += wb_check_background_flush(wb);
clear_bit(BDI_writeback_running, &wb->bdi->state);
return wrote;
}
static long wb_writeback(struct bdi_writeback *wb,struct wb_writeback_work *work)
{
for (;;) {
if (work->sb)
progress = writeback_sb_inodes(work->sb, wb, work);
static long writeback_sb_inodes(struct super_block *sb,struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
while (!list_empty(&wb->b_io)) {
struct inode *inode = wb_inode(wb->b_io.prev);
__writeback_single_inode(inode, &wbc);
static int__writeback_single_inode(struct inode *inode, struct writeback_control *wbc)
{
struct address_space *mapping = inode->i_mapping;
ret = do_writepages(mapping, wbc);
if (wbc->sync_mode == WB_SYNC_ALL) {
int err = filemap_fdatawait(mapping);
if (ret == 0)
ret = err;
}
int do_writepages(struct address_space *mapping, struct writeback_control *wbc){
int ret;
if (wbc->nr_to_write <= 0)
return 0;
if (mapping->a_ops->writepages)
ret = mapping->a_ops->writepages(mapping, wbc);
最终调用a_ops->writepages刷入pages到磁盘设备。
DFPC的BDI压缩代码详解
bool FRFCFS::BDICompress (NVMainRequest *request, uint64_t _blockSize, bool flag )
{ //blocksize就是req的data的size,一般是64 uint64_t * values = convertByte2Word(request, flag, _blockSize, 8); //把64个指向uint8的转换成8个word,一个word64bits //也就是论文中说的8*8bytes uint64_t bestCSize = _blockSize; uint64_t currCSize = _blockSize; uint64_t i, pos, bestPos; uint64_t words[35]; uint64_t wordPos[35]; //0~8 chars uint64_t currWords[35]; uint64_t currWordPos[35]; //0~8 chars bool comFlag = false; bestPos = 16; //看一条cacheline内的差别,比较value[0]~value[8] if( isSameValuePackable( values, _blockSize / 8)) { currCSize = 8; } if(bestCSize > currCSize) { //这里我也不太清楚到底是哪种,大概是全0或者全相同中的一种 //bestPos就=2,word[1]为value高32bits,word[2]低32bits bestCSize = currCSize; bestPos = bestCSize / 4; //假设这个value 1-8是一样的, bestPos = 8/4 =2 words[0] = 0x0; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = (values[i/2] >> (32*(1-i%2))) & 0xFFFFFFFF; wordPos[i+1] = 8; } //words[1] = value[0]高32位 //words[2] = value[0]低32位 bestPos++; //然后bestPos=3 } //传入values,8,1,8,.... //总共有size = 8个,blimit=1,bsize=8为不压缩的大小 currCSize = multBaseCompression( values, _blockSize / 8, 1, 8, currWords, currWordPos, pos); if(bestCSize > currCSize) { bestCSize = currCSize; bestPos = pos; words[0] = 0x1; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = currWords[i]; wordPos[i+1] = currWordPos[i]; } bestPos++; } currCSize = multBaseCompression( values, _blockSize / 8, 2, 8, currWords, currWordPos, pos); if(bestCSize > currCSize) { bestCSize = currCSize; bestPos = pos; words[0] = 0x2; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = currWords[i]; wordPos[i+1] = currWordPos[i]; } bestPos++; } currCSize = multBaseCompression( values, _blockSize / 8, 4, 8, currWords, currWordPos, pos); if(bestCSize > currCSize) { bestCSize = currCSize; bestPos = pos; words[0] = 0x3; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = currWords[i]; wordPos[i+1] = currWordPos[i]; } bestPos++; } free(values); values = convertByte2Word(request, flag, _blockSize, 4); if( isSameValuePackable( values, _blockSize / 4)) { currCSize = 4; } if(bestCSize > currCSize) { bestCSize = currCSize; bestPos = bestCSize / 4; words[0] = 0x4; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = (values[i/2] >> (32*(1-i%2))) & 0xFFFFFFFF; wordPos[i+1] = 8; } bestPos++; } currCSize = multBaseCompression( values, _blockSize / 4, 1, 4, currWords, currWordPos, pos); if(bestCSize > currCSize) { bestCSize = currCSize; bestPos = pos; words[0] = 0x5; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = currWords[i]; wordPos[i+1] = currWordPos[i]; } bestPos++; } currCSize = multBaseCompression( values, _blockSize / 4, 2, 4, currWords, currWordPos, pos); if(bestCSize > currCSize) { bestCSize = currCSize; bestPos = pos; words[0] = 0x6; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = currWords[i]; wordPos[i+1] = currWordPos[i]; } bestPos++; } free(values); values = convertByte2Word(request, flag, _blockSize, 2); currCSize = multBaseCompression( values, _blockSize / 2, 1, 2, currWords, currWordPos, pos); if(bestCSize > currCSize) { bestCSize = currCSize; bestPos = pos; words[0] = 0x7; wordPos[0] = 1; for(i = 0; i < bestPos; i++) { words[i+1] = currWords[i]; wordPos[i+1] = currWordPos[i]; } bestPos++; } free(values); values = NULL; if(bestCSize < _blockSize) { comFlag = true; Word2Byte(request, flag, bestPos, bestCSize, words, wordPos); } return comFlag;}
转载地址:http://grkii.baihongyu.com/