cache和top buff cache的区别

buff和cache的区别_百度知道
buff和cache的区别
我有更好的答案
您可能关注的推广
cache的相关知识
等待您来回答
下载知道APP
随时随地咨询
出门在外也不愁如何影响程序性能_SOHU-DBA_【传送门】
男子留学海外三年携外籍妻儿回家过年CPU Cache
如何影响程序性能CPU Cache 是CPU 中用于减少平均访问内存时间的高速存储器件。较内存使用的DRAM ,cache 使用的SRAM 速度更快、价格也更高,所以cache 容量一般较小。与cache 相关的概念有:cache line,associativity(相连性),L1 cache,L2 cache ,cache 命中,cache 失效等。cache 按层次分为L1、L2 甚至L3级cache,速度依次递减,容量依次增大。CPU 直接能够读取的只有L1 cache。在大多数处理器包括Intel CPU 中,cache按照缓存的指令或者数据分为数据缓存和指令缓存(如L1d 和L1i) 。在多核的CPU 中,每个核心具有独立的L1 级cache,往往公用L2 级cache。公用cache 可能存在多线程缓存污染等问题。cache 和内存传输的最小单位是cache line,一般大小为64 bytes。cache 每次从内存读取或者写入cache line 大小的数据,而不是我们在程序中定义的数据结构的大小。cache lines 之间的替换常用的是LRU 算法。如果CPU 读取的数据在cache 中,则cache 命中(cache hit),否则cache 失效(cache miss)。每一级的cache 失效将到下一级cache 中寻找数据,直至内存。cache miss 是有开销的,小于访存时间,大于cache hit 时间。每次cache 失效,CPU 就会处于停滞(stall)状态,直至缓存读取到所需要的数据。因为cache 容量远小于内存(有种说法是1:1000),内存和cache 之间存在映射关系(associativity)。按照映射关系不同,分为:全相连映射(fully associative),多路组相连映射(N-way set associative)和直接映射(directly mapped)。全相连映射缓存和内存是全映射关系,内存中任何一块数据都可以没有限制地放的任何一个cache line 中,缓存利用率高,但设计复杂未被采用;直接映射将一块内存地址映射到一个cache line(the number of cache line = memory address%number of cache lines,其中内存地址以cache line 大小为单位),直接映射设计起来简单,但利用率低;多路组相连映射缓存更为常见,是设计复杂度和性能的权衡。它将一块内存映射到不同路上的cache lines 中,如果是8-路组相连,则一块内存映射到8-路上的cache lines 上(the number of cache line = memory address%(number of cache lines/number of ways)。不同路上的负责缓存相同内存的缓存构成一个集合set,不同set 缓存不同内存地址的数据。number of set = cache size/(ways of associativity * size of cache line)。下图是两路组相连映射(Two-way set associative)的一个例子:将给出几个缓存相关的程序用例,实验用例的环境如下:CPU : Intel(R) Xeon(R) CPU E5620 @ 2.40GHz 32KB L1 cache,12MB L2 cacheOS : 2.6.35-22-generic #33-UbuntuGCC Version : 4.4.5compiler flags : -O3 -msse4 -DINTEL_SSE4 -Wall -std=c99Example 1 : Cache Line Cache line 的大小一般是64 Bytes,如果程序连续访问的数据在同一个cache line 中将减少cache miss 的次数。下面的程序以step 步为间隔访问64MB 数据:
unsigned int *
unsigned long long buff_size = 64*;
gettimeofday(&start, NULL);
for(unsigned long long i = 0; i < buff_ i=i+step){
pbuff[i] += 3;
gettimeofday(&end, NULL);结果如下图:图中步长在16 时出现拐点。在step < 16 时,并没有随着步长增加而减少运行时间,这是因为:unsigned int 类型大小为 4 Bytes,程序中如果步长小于16(16*4Bytes = 64Bytes)的话,步长较少只会增加cache 的命中,而cache 命中时间相对于从内存读取数据可忽略不计。而只有step>16 时,才能够减少因为从内存读取到缓存的数据量,从而减少运行时间。Example 2 : Cache Size cache 的大小对性能的影响巨大,下面的代码针对不同的buff_size 测试读写速度:unsigned char *for(int j = 0 ; j < ++j){for(unsigned long long i = 0; i < buff_ i=i+64){
(*(pbuff+i))++;
}程序迭代访问一段buff ,计算访问速度,步长为64Bytes(size of cache line)。在L1 cache 大小之前,速度都在50GB/s 速度以上。L2 cache 大小在12MB ,虽然在12MB 有下降沿,但不是很明显,这和编译器有关。但在32MB 以后速度基本上维持在3 GB/s 左右,大致为访存速度。Example 3 : Cache Association cache 的关联性也会影响到程序的性能,因为不同set 的cache 是负责不同内存块的。如果频繁访问某些内存块会导致某些sets of cache 经常被替换,而其余sets 没有被利用。下面的程序按照不同步长循环访问固定大小的buff,循环的次数iters 固定,因此cache miss 影响了系统运行时间unsigned char *for(unsigned long long i = 0; i < ++i){
(*(pbuff+p))++;
if(tmp>buff_size){p=0;}else{p=}}下面测试时buff 大小为32MB,循环运行229 得到的结果,L2 cache miss 对运行时间的影响非常明显,在512、1024、 等步长都出现了尖锐的峰值(原因在下面一个例子中分析)。下图是buff 大小为33KB(大于L1 cache 32KB),循环运行229 的运行时间,在步长128,256,512 都出现了尖锐的峰值。我们的cache 是32KB,8路组相连,64 个cache sets,每个set 大小为512B(8 条cache line),那么相同set 映射的内存间隔为32KB/8 = 64×64B= 4KB。所以步长为4KB 的访问实际上访问的是位于同一个cache set 中的数据,访问集中在一个set 中导致大量的cache miss。同理步长为4KB的约数大小(如1KB,512B)时,访问会集中在2/4/8/.. 等cache sets 中,同样会因为LRU 导致大量cache miss 使得运行时间变长。这种内存间隔长度在Agner Fog 的 Optimizing software in C++ 中有提到:critical_stride = cache_size/number_of_ways = number_of_sets * line_sizeExample 4 : Matrix Inversion 下面是一个更实际的例子,转置一个正方矩阵:#include
#include下图则给出了转置一个矩阵随着矩阵大小增大所需要的时间: 随着正方矩阵的增加,所需要的时间不断增加毋庸置疑,但“奇怪”的是512×512 大小的正方矩阵处出现了一个峰值。仔细分析下就知道还是critical_stride惹的祸。代码中inverse_matrix 中的循环执行的是置换matrix[i][j] 和matrix[j][i] 的值,其中matrix[i][j] 是按照内存顺序访问第i 行的元素,而matrix[j][i] 每次访问第i 列的所有元素。这些元素单独占据一个cache line,而且间隔为512*4Bytes = 2KB。也就是说同列上相间隔的一个元素差距为半个critical_stride ,同列上所有元素在内存上只映射到两个sets 16 个cache line,而一列中有512 个元素,这样就导致了在访问列上的元素时导致大量cache miss。使得行上的顺序访问无法利用到列上已经访问缓存在cache 上的元素。同理在256×256 和128×128 处也有两个峰值:126
24.792000 us
110.082000 us 127
25.243000 us
110.596000 us 128
43.110000 us
225.817000 us 129
25.892000 us
104.745000 us 130
26.431000 us
107.871000 us不用奇怪在 处为啥没有也出现峰值,因为当矩阵增大到一定程度时,没有足够的L1 cache 缓存单列上的元素,从而导致matrix[i][j] 是按照内存顺序访问第i 行的元素也无法利用到列上元素的缓存,这是由于L1 cache 大小决定的,而不是因为列上元素只映射到少量的sets 中导致的,因此矩阵足够大时不会再有峰值。参考:CPU Cache WIKIgallery-of-processor-cache-effectsFunctional Principles of Cache MemoryWhy speed of memcpy() drops dramatically every 4KB?Speed of memcpy() greatly influenced by different ways of malloc()Why is transposing a matrix of 512×512 much slower than transposing a matrix of 513×513?Page Cache_百度百科
Page Cache
本词条缺少概述、名片图,补充相关内容使词条更完整,还能快速升级,赶紧来吧!
一、page cache简介
page cache,又称pcache,其中文名称为页,简称页高缓。page cache的大小为一页,通常为4K。在读写文件时,它用于缓存文件的逻辑内容,从而加快对磁盘上映像和数据的访问。
二、page cache的功能详解
在从外存的一页到内存的一页的映射过程中,page cache与buffer cache、swap cache共同实现了高速缓存功能,以下是其简单映射图,
外存的一页(分解为几块,可能不连续)
物理磁盘的磁盘块
内存的buffer cache
内存的一页(由一个划分的几个连续buffer cache构成)
页高缓系统
在这个过程中,内存管理系统和VFS与page cache交互,内存管理系统负责维护每项page cache的分配和回收,同时在使用memory map方式访问时负责建立映射;VFS负责page cache与的数据交换。
三、page cache的管理
在Linux内核中,文件的每个最多只能对应一个page cache项,它通过两个数据结构来管理这些cache项,一个是radix tree,另一个是。
Radix tree是一种搜索树,Linux内核利用这个数据结构,快速查找脏的(dirty)和回写的(writeback)页面,得到其文件内偏移,从而对page cache进行快速定位。图1是radix tree的一个示意图,该radix tree的分叉为4(22),树高为4,用来快速定位8位文件内偏移。
另一个数据结构是双向链表,Linux内核为每一片区域(zone)维护active_list和 inactive_list两个双向链表,这两个list主要用来实现物理内存的回收。这两个链表上除了文件Cache之外,还包括其它匿名 (Anonymous)内存,如进程等。
四、page cache相关API及其实现
Linux中与文件Cache操作相关的API有很多,按其使用方式可以分成两类:一类是以拷贝方式操作的相关接口,如read/write/sendfile等,其中sendfile在2.6系列的内核中已经不再支持;另一类是以方式操作的相关接口,如mmap等。
第一种类型的API在不同文件的Cache之间或者Cache与应用程序所提供的buffer之间拷贝数据,其实现原理如图2所示。
第二种类型的API将Cache项映射到用户空间,使得应用程序可以像使用内存一样访问文件,Memory map访问Cache的方式在中是采用请求页面机制实现的,其工作过程如图3所示。
首先,应用程序调用mmap(图中1),陷入到内核中后调用do_mmap_pgoff(图中2)。该函数从应用程序的中分配一段区域作为映射的,并使用一个VMA(vm_area_struct)结构代表该区域,之后就返回到应用程序(图中3)。当应用程序访问mmap所返回的地址指针时(图中4),由于虚实映射尚未建立,会触发(图中5)。之后系统会调用缺页中断处理函数(图中6),在缺页中断处理函数中,内核通过相应区域的 VMA结构判断出该区域属于文件映射,于是调用具体文件系统的接口读入相应的Page Cache项(图中7、8、9),并填写相应的虚实映射表。经过这些步骤之后,应用程序就可以正常访问相应的内存区域了。
Page Cache加入/离开page cache还涉及到如下几个函数
add_page_to_hash_queue /*加入pache cache hash表*/
add_page_to_inode_queue /*加入inode queue即address_space*/
remove_page_from_inode_queue
remove_page_from_hash_queue
__remove_inode_page /*离开inode queue和hash 表*/
remove_inode_page /*同上*/
add_to_page_cache_locked /*加入inode queue,hash 和lru cache*/
__add_to_page_cache /*同上*/
仅罗列函数add_page_to_hash_queue,以示完整:&FONT
color=green&
static void add_page_to_hash_queue(struct page * page, struct page **p)
struct page *next = *p;
/* page-&newNode
page-&next_hash = /*
page-&pprev_hash = /* p--& |hashp|--&|oldNode| */
next-&pprev_hash = &page-&next_
if (page-&buffers)
PAGE_BUG(page); /*证明page 不会同时存在于page cache
和 buffer cache*/
/*2.6 已经与此不同了*/
atomic_inc(&page_cache_size);
Page Cachepage cache 和 inode
page cache 在代码中又称 inode page cache, 足以显示page cache 和inode紧密关联.加入page cache 和加入inode cache是同一个意思.加入page cache意味着同时加入page cache hash表和inode queue(也建立了page和addr sapce的关系). 见函数add_to_page_cache_locked,__add_to_page_cache即可取证.从page cache 删除在程序中叫__remove_inode_page,再次显示inode 和page cache的&一体化&.
企业信用信息【转载】C6000系列的C64x+ Cache优化--配置,Cache miss和Cache一致性 - 博客频道 - CSDN.NET
nature_XD的专栏
分类:嵌入式开发~~循序蛮进
本文主要介绍TI C6000系列的C64x&#43;的DSP Cache配置,常见的Cache miss模式以及如何在多核或者多外设DMA系统下维护Cache一致性,当然还有Cache优化的专题,针对指令L1P和数据L1D
cache的优化。
C64x&#43;与C64x CACHE的区别
Write Buffer(WB):
1. C64x&#43;的 write buffe(WB)宽度为128-bit; 而C64x WB宽度为64-
可cache性(Cacheability):
2. 对C64x&#43;,外部内存地址通过MAR-bit配置其可cache性,只对L1D和L2有效,即对指令cache来说,只会cache到L1P级;但对C64x来说,外部内存地址配置其cache性后,对L1P,L1D和L2均有效;
3. 对C64x&#43;,整个外部内存地址均可配置成可cache性;但对C64x,仅部分地址段可以;
Snooping protocol:
4. 对C64x&#43;,其保持cache一致性机制更为有效,因为它直接将数据发送到L1D CACHE和DMA,消除了因invalidates导致的大量cache miss,而C64x通过invalid 和writeback cache line来维持一致性,效率显然低些;(C64x&#43;在修改数据的同时就进行了一致性维护!)
5. C64x&#43;的snoop 一致性协议不能维护L1P cache和L2 SRAM之间的一致性,这个工作需要程序员来完成,而C64x可以做到;
Cache 一致性操作:
6. 对C64x&#43;,即使L2 cache被disable了,L2仍然总是根据L1P 和L1D的变化来对L2 进行cache一致性维护。对C64x则不是这样,它需要明确使用L1一致性操作指令来对对L2进行维护;
7. C64x&#43;支持完全的L1D cache一致性操作,相对应,C64x仅支持L1D范围内的invalidate 和writeback-invalidate;
8. 当cache大小改变时,C64x&#43;在初始化新尺寸cache时会自动writeback-invalidate cache,相反,C64x需要程序员发出明确的writeback-invalidate指令;
9. C64x&#43;,L2 cache不包含(noninclusive)L1D和L1P。这意味从L2牺牲的行不会造成L1P和L1D的对应行也被牺牲。然而,对C64x却是如此。noninclusive的优势是由于取指在L2中分配的行不会牺牲L1D cache中的数据行,由于取数在L2中分配的行不会造成L1P中的指令行被牺牲掉。这有利于减少cache misses。
10. C64x&#43;在L1和L2之间添加了一条高带宽的DMA通道(IDMA),用于有效的将数据从L1 SRAM搬进、搬出。(SPRU871 有IDMA的详细介绍)
11. 由于C64x&#43;上miss会导致大量的延时stall,因此,消除miss和充分利用miss pipelining变得非常重要。对C64x&#43;来说,有了L1 SRAM、比较大的L1 cache容量、low-overhead snooping和L2 cache的noninclusivity, 这点相应变得容易了。
12.&利用L1D的miss pipelining对改善性能非常的重要。对C64x,数据miss pipelining可以减少4个对C64x&#43;,数据miss pipelining可以减少7.5到9个stall circles.
13. 通过C64x&#43;带宽管理,不同请求间的访问冲突和bank冲突得以解决。(带宽管理详见:TMS320C64x&#43; DSP Megamodule Reference Guide)。
14. C64x&#43; cache控制器支持cache冻结模式,该模式可以阻止新行的分配。对防止L1P cache中经常重复使用的code被牺牲掉,这一点特别有用。
Cache miss类别:
区别 miss的类型和发生miss的原因可以帮助选择恰当的措施来减少或避免miss。通常有三种miss类型。
1. conflict miss:因为冲突导致数据驱逐,即,CPU当前访问的内存位置,与刚刚被cached的内存位置映射到同一set,这样,因为冲突,该行在被重用前就被驱逐掉了,导致miss发生。正是因为conflict miss的发生,才引申出后面set-associative的解决方案。
2. capacity miss:在conflict miss发生时,如果是因为cache容量过小形成exhausted,当Miss发生时所有line frames均已经分配完了,这样导致的miss即为capacity misss。解决capacity miss的方法一般是减少每次操作的数据量,从硬件设计上来说,尽量增大cache容量。
3. compulsory miss:也叫做&第一次参考miss&。当数据第一次从下级内存被cache时发生。这种miss是无法避免的,故称为compulsory miss.
Cache一致性
一、配置配置:
配置:启动时默认状态下被全部为。如果启用了,则被自动使能;否则,可以通过调用命令:来使能。
外部内存的和可以通过调用命令修改对应来控制外部内存段的而对,外部内存总是的,跟无关。
注意:配置成的地址段就不能再放入因为链接的时候,是不包含地址段的,如果要使用或,则应该相应减小段大小。
第一个配置成可
二、一致性问题
如果内存是共享的,可以访问的,并且被修改了,那么这块内存就存在一致性维护的问题。对目前我们简单的编解码器移植来说,存在与的共享,但不存在同时修改的问题,故无需一致性维护。对来说,其控制器可以根据以硬件一致性协议来自动维护由所访问数据的一致性。当发起读和写命令时就激活了一致性维护机制。当读内存时,数据直接从提交给,而不会在更新;当写时,数据直接提交到,并且更新。
除了一致性操作,对于最好将其按对齐,并保证其为的整数倍,可以通过以下方式保证:
我们也可以调用宏定义数组来完成上述功能,第一个参数是类型,可以为
一行在中到不一定在中也能到;一行可能从中牺牲掉但中仍然保存。
三、Run-time中改变Cache配置
&Disabling External Memory caching
&&&&& 通常不会出现这种需要。如果要做的时候应该考虑到以下问题:若MAR从1改为0,外部内存中原本被cached的地址仍然在cache中(cache的拷贝性),所以以cache方式访问这些外部地址仍然会hit。只有当L2 miss,或L2全SRAM时(这种状况也可理解为L2 miss),要访问外部内存时,更改的MAR比特才起作用.
&changing cache sizes during RUN-TIME
&&&&& 以L2的应用为例。有2个任务:A/B, 对A,64K L2 SRAM最好;对B,32K L2 cache/32K L2 SRAM最好。将64K L2分成两段32K,假定第二个32K放着A的程序、一些全局变量(B执行阶段需要保存下来),以及A的一些变量,这些变量在任务切换后不再需要。
&&&&& 用DMA将需要保存下来的代码和全局变量搬到外部内存某一区域,这样,就可以切换cache模式了。cache控制器会在对新尺寸的cache做初始化前将所有cache lines自动writeback-invalidate。注意:更改L2 cache大小不会造成L1P/L1D cache的任何evictions。尺寸修改可以调用CACHE_setL2Size()完成。
&&&&& 任务B执行完毕后,需要切换回A配置,B时的32K L2 cache 将需要切换回SRAM,之前必须将该32K line frames回写到外部内存并invalidated。当cache size切换时,这些工作由cache控制器自动完成。这样,可以从片外将A的代码和一些全局变量再拷贝回原来位置。
&&&&& 以上应用同样适用于L1P/L1D。对应的procedure、linker command file和C code example如下:
Procedure:
More Cache (SRAM转化成cache)
1. DMA or copy needed code/data out of SRAM addresses to be converted to cache.
2. Wait for completion of step 1.
3. Increase cache size using CACHE_setL1pSize(), CACHE_setL1dSize(),or CACHE_setL2Size()
Less Cache(cache转化成SRAM)
1. Decrease Cache size using CACHE_setL1pSize(),CACHE_setL1dSize(),or CACHE_setL2Size()
2. DMA or copy back any code/data needed.
3. Wait for completion of step 2.
linker command file:
&&& MEMORY
&&&&& L2_1: o = h&& l = h&& /*1st 32K segment: always SRAM */
&&&&& L2_2: o = h&& l = h&&& /*2nd 32K segment:Task A-SRAM,Task B-Cache */
&&&&& CE0 : o = h&& l = h&& /*external memory */
&&& SECTIONS
&&&&& .cinit&&&&&& & L2_1
&&&&& .text&&&&&&& & L2_1
&&&&& .stack&&&&& & L2_1
&&&&& .bss&&&&&&&& & L2_1
&&&&& .const&&&&& & L2_1
&&&&& .data&&&&&& & L2_1
&&&&& .far&&&&&&&&& & L2_1
&&&&& .switch&&&& & L2_1
&&&&& .sysmem & L2_1
&&&&& .tables&&& & L2_1
&&&&& .cio&&&&&&&& & L2_1
&&&&& .sram_state_A&&&&&&& & L2_2
&&&&& .sram_process_A&& & L2_2
&&&&& .sram_local_var_A & L2_2
&&&&& .external&&&&&&&&&&&&&&& & CE0
一、cache性能特点
&&&&& 优异的cache性能很大程度上依赖于cache lines的重复使用,优化的最主要目标也在于此,一般通过恰当的数据和代码内存布置,以及调整CPU的内存访问顺序来达到此目的。由此,应该熟悉cache内存架构,特别是cache内存特点,比如line size, associativity, capacity, replacement scheme,read/write allocation, miss pipelining和write buffer.另外,还需要知道什么条件下CPU STALLS会发生以及产生延时的cycle数。只有清楚了这些,才能清楚如何优化cache。
二、优化cache
&&&&& L1 cache的特点(容量、associativity、linesize)相对于L2 cache来说更具局限性,优化了L1 cache几乎肯定意味着L2 cache也能得到有效使用。通常,仅优化L2 cache效果并不理想。建议将L2 cache用于一般的类&#20284;控制流程等大量内存访问无法预测的部分。L1和L2 SRAM应该用于时间性非常重要的信号处理算法。数据能够用EDMA或IDMA直接导入L1 SRAM,或用EDMA导入L2 SRAM。这样,可使L1 cache的mem访问效率获得优化。
&&& 有两种重要方法来减少cache ovehead:
1. 通过以下方式减少cache miss数量(L1P,L1D,L2 cache):
&&& a. cache line reuse最大化
&&&&& &访问cached行中的所有mem位置(应该是对多路组相联才有效,直接映射地址是一对一的)。进入cache行中的数据花费了昂贵的stall cycles,应该被使用;
&&&&& &cached line中的同一内存位置应该尽可能的重复使用。
&&& b. 只要一行被使用,将要避免牺牲该行
2. 利用miss pipelining,减少每次miss的stall cycles数
&&& cache优化的最好策略是从上到下的方式,从应用层开始,到程序级,再到算法级别的优化。应用层的优化方法通常易于实现,且对整体效果改善明显,然后再配合一些低层次的优化策略。这也是通常的优化顺序。
应用层级应考虑的几点:
&用DMA搬进/出数据,DMA buffer最好分配在L1或L2 SRAM,出于以下考虑。首先,L1/L2 SRAM更靠近CPU,可以尽量减少延迟;其次,出于cache一致性的考虑。
&L1 SRAM的使用。C64x&#43;提供L1D 和L1P SRAM,用于存放对cache性能影像大的代码和数据,比如:
&&& @ 至关重要的代码或数据;
&&& @ 许多算法共享的代码或数据;
&&& @ 访问频繁的代码或数据;
&&& @ 代码量大的函数或大的数据结构;
&&& @ 访问无规律,严重影像cache效率的数据结构;
&&& @ 流buffer(例如L2比较小,最好配置成cache)
&&& 因为L1 SRAM有限,决定哪些代码和数据放入L1 SRAM需要仔细考虑。L1 SRAM 分配大,相应L1 cache就会小,这就会削弱放在L2和外部内存中代码和数据的效率。 如果代码和数据能按要求导入L1 SRAM,利用代码和/或数据的重叠,可以将L1 SRAM设小点。IDMA能够非常快的将代码或数据page到L1。如果代码/数据是从外部page进来,则要用EDMA。但是,非常频繁的paging可能会比cache增加更多的overhead。所以,必须在SRAM和cache大小之间寻求一个折中点。
&区别signal processing 和 general-purpose processing 代码
&&& 后者通常并行性不好,执行过程依赖于许多条件,结果大多无法预测,比如滤波模块,数据内存访问大多随机,程序内存访问因分支条件而异,使得优化相当困难。鉴于此,当L2不足以放下整个代码和数据时,建议将其代码和数据放到片外,并允许L2 能cache访问到。这样腾出更多的L2 SRAM空间存放易于优化,结构清晰的前者代码。由于后者代码的无法预测性,L2 cache应该是设的越大越好(32k~256k). 前者比较有规律的代码和数据放到L2 SRAM或L1 SRAM更为有利。放到L2,可以允许你根据CPU对数据的访问方式来修改算法,或调整数据结构,以获得更好的cache友好性。放到L1
SRAM,无需任何cache操作,并且除非bank冲突,无需做memory 优化。
procedural级的优化:
&&& 优化目的是减少cache miss,以及miss带来的stall数。前者可通过减少被cache的内存大小并重复使用已经cached lines来获得。尽量避免牺牲行并尽可能写已经分配的行可以提高重用率。利用miss pipelining可以减少stall数。以下根据三种不同类型的读miss来分析优化的方法。
&选用合适的数据类型,以减少内存需要
&&& 16位可以表示的数不要定义成32位,这不但可以省一半内存消耗,而且减少compulsory miss。这种优化容易修改,无需改动算法,而且小数据类型容易实现汇编的SIMD。在不同系统平台端口间的数据流动,容易出现这种低效的数据类型。
&&& 前一算法的输出是后一算法的输入。如果输出、输入不是同一级内存地址,后一算法使用前一算法结果时就存在读miss的消耗。这个时候就要考虑两者空间如何布置。如果超过两个数组映射到L1D的同一个set,则会产生conflict miss(L1D cache是2-way set-associative),故应该将这些数组连续分配(why???)(详见P55)
&避免L1P conflict miss
&&& 即使cpu需要的指令全在L1P cache(假定无capacity miss),仍然可能会产生conflict miss。以下解释conflict miss是如何产生的,又如何通过code在内存中的连续存放来消除miss。例如:
&&& for(i=0; i&N; i&#43;&#43;)
&&& {&& function_1();&& function_2(); }
&&& 如果func2在L2中的位置正好与func1有部分处于同一set中,而L2 cache是4-way set-associativity,处于同一set的指令在被L1P cache循环读取后,可能会出现conflict miss(如刚读入func1,然后读入func2,可能会驱逐掉func1在L1P中的部分cache lines).这种类型的miss是完全可以消除掉的,通过将这两个函数的代码分配到不冲突的set中,最直接具体的方法是将这两个函数在内存中连续存放,存放的方法有二:
1. 使用编译器选项 -mo,将各C和线性汇编函数放到各自独立的section,其中汇编函数必须被放到以.sect标示的sections中。然后检查map file,获取各函数的段名,比如上例.text:_function1和.text:function_2。则linker命令文件如下:
&&& ...&&&
&&& SECTIONS
&&& { .cinit&& & L2SRAM
&&&&&&& .GROUP&& & L2SRAM&&&&& (在CCS3.0及以后,.GROUP标示用于强制指定段的link顺序)
&&&&&&& {&& .text:_function1 .text:function_2
&&&&&&&&&&& .text
&&&&&&& .stack&& & L2SRAM
&&&&&&& .bss&&&& & L2SRAM
&&&&&&& ...
&&& linker会严&#26684;按照GROUP申明的顺序来link各段。上例中,先func1,然后是func2,然后是.text section中的其它函数。但要注意,使用-mo后会导致整个code尺寸变大,因为包含code的任何段都要按32-byte边界对齐。
2. 为避免-mo只能指定section,而不能单独指定函数的不足,如果仅需要函数连续排放,我们可以在定义函数前,通过#pragma CODE_SECTION来为函数指定sections:
&&& #pragma CODE_SECTION(function_1,&.funct1&)
&&& #pragma CODE_SECTION(function_2,&.funct2&)
&&&& void function_1(){...}
&&&& void function_2(){...}
&&& 这样,linker命令文件如下:
&&& SECTIONS
&&&&&& .cinit & L2SRAM
&&&&&& .GROUP & L2SRAM
&&&&&&&& {
&&&&&&&&&&&& .funct1.funct2
&&&&&&&&&&&& .text
&&&&&&&& }
&&&&& .stack & L2SRAM
&&& 结合上例可见,在同一循环里面或在某些特定时间帧里面反复调用的多个函数,需要考虑重排。如果L1P cache不够大,不足以放下所有循环内函数,则循环必须被拆开来,以保证code无驱逐的重用。但这会增大内存消耗,上函数拆分成如下:
&&& for (i=0; i&N; i&#43;&#43;)
&&&&& {&&& function_1(in[i], tmp[i]);&& }&&&&& //&#43;&#43;很显然需要增大tmp[],以保存func1每
&&& for (i=0; i&N; i&#43;&#43;)&&&&&&&&&&&&&&&&&&&&&&&&& //&#43;&#43;次循环的输出结果,作为func2的输入
&&&&& {&&& function_2(tmp[i], out[i]); }
&freezing L1P cache
&&& 调用CSL函数: CACHE_freezeL1p()与CACHE_unfreezeL1p()可以控制L1P cache,阻止其分配新行,freezing后,cache内容就不会因conflict而牺牲,但其他所有如dirty比特、LRU更新、snooping等等cache行为仍然是一样的。肯定会被重用的code,如果因为其他仅执行一次的code而被驱逐掉,比如中断程序等,可以采用这个函数来避免。
&避免L1D conflict miss
&&& L1P是直接映射型cache,如果cpu访问的地址没有包含在同一cache line内,则会相互evict。然而,L1D是2-way set-associative,对直接映射来说是conflict 的两lines却能够同时保存在cache中,只有当第三个被访问分配的memory地址仍映射到同一set时,早前分配的两个cache lines将根据LRU规则牺牲掉一行。L1D的优化方法与上面L1P类&#20284;,区别在于前者是2-way set-associative,而后者是direct-mapped,这意味着对L1D,两个数组能够映射到同一set,并同时保存在L1D。
&&&&@定义数组后,通过编译选项-m生成map file可以查看给该数组分配的地址。
&&& 与L1P类&#20284;,如果不连续定义数组,会导致各种miss(具体各数组是如何映射到L1D cache各way各set的,没看明白,P61),为避免读miss,应在内存中连续分配各数组。注意,因为linker的内存分配规则,在程序中连续定义数组,并不表示他们在内存中的地址也是连续的(比如,const数组会放在.const section而非.data section中)!因此,必须将数组指定到用户定义的段:
&&& #pragma DATA_SECTION(in1, &.mydata&)
&&& #pragma DATA_SECTION(in2, &.mydata&)
&&& #pragma DATA_SECTION(w1, &.mydata&)
&&& #pragma DATA_SECTION(w2, &.mydata')
&&&&#pragma DATA_ALIGN(in1, 32)&&&& //&#43;&#43; 数组按照cache line边界对齐
&&&& short in1 [N];
&&&& short in2 [N];
&&&& short w1 [N];
&&&& short w2 [N];
&&&&@另注意:为避免memory bank冲突,非常有必要将数组按不同memory bank对齐,如:
&&& #pragma DATA_MEM_BANK(in1, 0)
&&& #pragma DATA_MEM_BANK(in2, 0)
&&& #pragma DATA_MEM_BANK(w1, 2)
&&& #pragma DATA_MEM_BANK(w2, 2)
&&& @利用miss pipelining可以进一步减少miss stalls。利用touch loop来为四个数组在L1D cache中预分配空间,因为数组物理连续,故只需调用一次touch程序:
&&& touch(in1, 4*N*sizeof(short));
&&& r1 = dotprod(in1, w1, N);
&&& r2 = dotprod(in2, w2, N);
&&& r3 = dotprod(in1, w2, N);
&&& r4 = dotprod(in2, w1, N);
&&& ====&touch loop的意义和实现:意义是为了最大限度实现miss piplining。如果连续访问mem,因为一次miss,会搬移一个cacheline,则随后的访问就会hit,miss不能实现overlap。因此,为获得stalls的完全重叠,可以考虑在一个cycle内同时访问两个新的cacheline,即按两个cachelines的间距遍历mem。TI提供的汇编函数&touch&,用于在L1D cache中预先分配长为length的数组buffer,它对每两个连续cache
lines 分别并行load一个byte。为避免bank conflict,这两个并行load之间偏移一个word。 (c64x采用基于LSB的mem bank结构,L1D分成8个bank,每个bank宽32-bit,共2K,这些bank均为single port输入,每个cycle允许一个访问,与c621x/c671x的单bank多输入口有区别。这样,对同一bank同时进行读和写访问,总是会造成stall,而同时对同一bank进行读或写,只要满足一定条件,就不会产生stall)。
&避免L1D thrashing
&&& 这种Miss情况下,数据集比cache大,连续分配,但数据不需要reused,发生conflict miss,但无capacity miss发生(因为数据不reused)。 对同一set发生两个以上的读miss,这样在该行全部数据被访问前就将该行驱逐掉了,这种情况就是thrashing.假定所有数据在mem中是连续分配的,这样只有当被访问的所有数据集超过L1D cache容量时才会发生thrashing.这种conflict miss是可以完全避免的,通过在mem中连续分配数据集,并嵌入一些多余数组,强制将数据交叉映射到cache
sets。比如:
&&& int w_dotprod(const short *restrict w, const short *restrict x, const short *restrict h, int N)
&&& { int i, sum = 0;
&&&&&& _nassert((int)w % 8 == 0);&&&&& //&#43;&#43;如果w[],x[],h[]三个数组在内存中都映射到
&&&&&& _nassert((int)x % 8 == 0);&&&&& //&#43;&#43;同一L1D cache set,则L1D thrashing发生。当前读入
&&&&&& _nassert((int)h % 8 == 0);&&&&& //&#43;&#43;的w,x被随后读入的h给替换掉了....
&&&&&& #pragma MUST_ITERATE(16,,4)
&&&&&& for (i=0; i&N; i&#43;&#43;)
&&&&&&&&&& sum &#43;= w[i] * x[i] * h[i];
&&&&&&&& }
&&& 处理办法是在w,x后填充一个cache行大小的数,使h[0]往下偏移一行,映射到下一set:
&&&&&& #pragma DATA_SECTION(w, &.mydata&)
&&&&&& #pragma DATA_SECTION(x, &.mydata&)
&&&&&& #pragma DATA_SECTION(pad, &.mydata&)
&&&&&& #pragma DATA_SECTION(h, &.mydata&)
&&&&&& #pragma DATA_ALIGN (w, CACHE_L1D_LINESIZE)
&&&&&&& short w [N];
&&&&&&& short x [N];
&&&&&&& char pad [CACHE_L1D_LINESIZE];
&&&&&&& short h [N];
&&& 对应linker命令文件如下指定:
&&&&&& ...
&&&&&& SECTIONS
&&&&&& { GROUP & L2SRAM
&&&&&&&&&&&& {&& .mydata:w
&&&&&&&&&&&&&&&& .mydata:x
&&&&&&&&&&&&&&&& .mydata:pad
&&&&&&&&&&&&&&&& .mydata:h&& }
&&&&&&&&& ...
&避免capacity miss
&&& 这种情况下,数据需要重用reused,但是数据集比cache大,造成capacity和conflict miss。通过分裂数据集,一次处理一个数据子集,可以避免这种miss,这种方法叫做blocking或tiling. 下面以例子说明原因和处理方法。点积函数,调用4次,一个参考矢量,四个不同的输入矢量。
&&& short in1[N];
&&& short in2[N];
&&& short in3[N];
&&& short in4[N];
&&& short w [N];
&&& r1 = dotprod(in1, w, N);
&&& r2 = dotprod(in2, w, N);
&&& r3 = dotprod(in3, w, N);
&&& r4 = dotprod(in4, w, N);
&&& 假定每个数组都是L1D cache容量的两倍,对w来说,除了第一次调用需要compulsory miss外,之后应该保存在cache里面重用是最合理的,但分析可知,当处理到N/4的输入数据时,最先进入cache的w就开始被驱逐了,这样w将会被反复多次读入cache,非常浪费。可以考虑加个循环,每次只处理N/4的数据,保证w在读进cache后,直到用完才驱逐,修改如下:
&&& for (i=0; i&4; i&#43;&#43;)&&&&&&&&&&&& {
&&&&&&& o = i * N/4;
&&&&&&& dotprod(in1&#43;o, w&#43;o, N/4);
&&&&&&& dotprod(in2&#43;o, w&#43;o, N/4);
&&&&&&& dotprod(in3&#43;o, w&#43;o, N/4);
&&&&&&& dotprod(in4&#43;o, w&#43;o, N/4);&& }
&&&&& 可以利用miss pipelining进一步减少read miss stalls.在每次循环开始用touch循环预先在cache分配w[],这样在每次调用点积函数前,需要的输入数组都准备好了:
&&& for (i=0; i&4; i&#43;&#43;)&&&&&&&&&&&&&&&&&&&&&&&&&&& {
&&&&&&& o = i * N/4;
&&&&&&& touch(w&#43;o, N/4 * sizeof(short));
&&&&&&& touch(in1&#43;o, N/4 * sizeof(short));
&&&&&&& dotprod(in1&#43;o, w&#43;o, N/4);
&&&&&&&&&&&&& touch(w&#43;o, N/4 * sizeof(short));&& //&#43;&#43;每次touch in[]前都要touch w[]是为了保证w[]为MRU,
&&&&&&&&&&&&& touch(in2&#43;o, N/4 * sizeof(short)); //&#43;&#43;以防访问顺序发生改变导致w[]被驱逐掉。
&&&&&&&&&&&&& dotprod(in2&#43;o, w&#43;o, N/4);
&&&&&&& touch(w&#43;o, N/4 * sizeof(short));
&&&&&&& touch(in3&#43;o, N/4 * sizeof(short));
&&&&&&& dotprod(in3&#43;o, w&#43;o, N/4);
&&&&&&&&&&&&&& touch(w&#43;o, N/4 * sizeof(short));
&&&&&&&&&&&&&& touch(in4&#43;o, N/4 * sizeof(short));
&&&&&&&&&&&&&& dotprod(in4&#43;o, w&#43;o, N/4);&&&&&&&&&&&&& }
&&& 另外,本例中为避免bank conflict,数组w[]和in[]应该对齐到不同的memory banks:
&&&&&&& #pragma DATA_SECTION(in1, &.mydata&)
&&&&&&& #pragma DATA_SECTION(in2, &.mydata&)
&&&&&&& #pragma DATA_SECTION(in3, &.mydata&)
&&&&&&& #pragma DATA_SECTION(in4, &.mydata&)
&&&&&&& #pragma DATA_SECTION(w , &.mydata&)
&&&&&&& #pragma DATA_ALIGN(w, CACHE_L1D_LINESIZE) //&#43;&#43;意味着已#pragma DATA_MEM_BANK(w, 0)
&&&&&&&& short w [N];
&&&&&&& #pragma DATA_MEM_BANK(in1, 2)&&&&&&&&&&&&&&&&&&&&&&& //&#43;&#43;avoid bank conflicts
&&&&&&&& short in1[N];
&&&&&&&& short in2[N];
&&&&&&&& short in3[N];
&&&&&&&& short in4[N];
&避免write buffer相关的stalls
&&&&& WB只有4个入口,且深度有限,如果WB满,而又出现写miss,则CPU就会stall直到有WB有空间为止。同时,read miss会使得write buffer完全停止,因此保证正确的read-after-write顺序非常重要(read miss需要访问的数据很可能仍然在WB中)。通过在L1D cache中分配输出buffer(事先将输出buffer cache进入L1D),可以完全避免WB相关的stalls,这样write操作会在输出buffer中hit,而非由WB写出。事实上,输出buffer是在循环执行过程中逐渐进入L1D的,在此过程中还是会存在read
&&&& void vecaddc(const short *restrict x, short c, short *restrict r, int nx)
&&&&&&&& {&&
&&&&&&&&&&&& for (i = 0 ; i & i&#43;&#43;)
&&&&&&&&&&&& r[i] = x[i] &#43;
&&&&&&&& }
&&&&&&&&&& short in[4][N];
&&&&&&&&&& short out [N];
&&&&&&&&&& short ref [N];
&&&&&&&&&& short c,
&&&&&&&&&& for (i=0; i&4; i&#43;&#43;)
&&&&&&&&& {
&&&&&&&&& vecaddc(in[i], c, out, N);
&&&&&&&&& r = dotprod(out, ref, N);
&&&&&&&&& }
C64x&#43; DSP 的带宽管理系统(BWM)
BWM的目的:确保某些请求(requestors)不会长时间的霸占总线,而阻止了来自c64x&#43; megamodule内源(resources)的请求。与c64x&#43;的内存保护类&#20284;,BWM可对整个c64x&#43; megamodule做全局定义,但由每个local c64x&#43; megamodule源来实现。由此,初始化BWM就包含了对每个c64x&#43; megamodule源共同寄存器堆进行编程。
BWM对以下4个源进行带宽保护:
&&& &Level 1 program(L1P) SRAM/cache
&&& &Level 1 data(L1D) SRAM/cache
&&& &Level 2(L2) SRAM/cache
&&& &memory-mapped registers configuration bus
&&& 以上列出的每个c64x&#43; megamodule源存在以下一些潜在请求,这些请求也是由BWM来管理:
&&& &CPU-initiated transfers: data access(load/store,etc) / program access
&&& &可编程的cache一致性操作(如:writeback): block-based&& / global
&&& &internal DMA(IDMA)-initiated transfers(and resulting coherency operations)
&&& &external-initiated slave DMA(SDMA) transfers(and resulting coherency operations)
&&& ※一句话,以上即表示c64x&#43; megamodule包含的源,以及这些源相关的一些请求的优先级由BWM来管理。
&&&&& BWM对以上源的带宽管理是通过优先级仲裁来实现的。当同时出现对一个源的多个请求,则优先级最高的请求首先获得访问权,以此来解决冲突。如果出现资源被连续抢占的情况,则由一个抢夺计数器(contention counter)来解决低优先级请求的服务,由它来保证每n个仲裁cycles必须响应一次低优先级请求,这个n可以通过对寄存器的MAXWAIT域编程来决定。资源请求受阻一次,则contention counter加1,当contention counter达到n时,则低优先级请求的优先级设为-1,并对该请求做出响应。因此,除了从高到低(0~8)9种优先级外,硬件还使用一个-1的优先级,表示某个传输请求因contention
counter耗尽到n,使得该请求优先级获得提高。但用户不可对BWM仲裁控制寄存器直接写入-1的优先级。
&&&&& 一系列寄存器(仲裁控制寄存器)来实现BWM。这些寄存器在以下blocks中实现:L1D,L2,extended memory controller(EMC)。对L1P来说,没有可编程的BWM寄存器,但在L1P控制器中有固定带宽管理特性。 对每个源(L1D,L2,EMC)都有一系列仲裁寄存器(CPUARBD/ IDMAARBD/ SDMAARBD/ UCARBD& MDMAARBE),每个寄存器对应一个不同的请求。对应每个源的一组寄存器有相同的初始化&#20540;,对大多数应用来说,CPUARBD/ IDMAARBD/
SDMAARBD/ UCARBD的初始化&#20540;不需要修改。但MDMAARBE定义的优先级用于c64x&#43; megamodule以外数据传输,可能需要根据对应系统手册做些调整,大多数情况下,MDMAARBE应该通过编程提高优先级(赋更小&#20540;)。
开发过程中的其他问题:
1. IDMA:对达芬奇平台来说,感觉IDMA好像并不那么实用,首先,需要同时配置多个外部寄存器,如QDMA的情况并不多;其次,IDMA的一个主要功能是实现local mem之间的数据、代码搬移,可是达芬奇L2仅有64K,就算全部配置成SRAM,空间也是有限的,能够允许在此过渡的数据量也很有限;
2. 其他:DM6446的DMA是封装在FC里来协调算法和应用程序之间对DMA资源的控制的,对于底层实现,TI也提供了EDMA3LLD(low level driver)来实现DSP算法对EDMA资源的申请和使用。&
Reference:
本文主要介绍TI C6000系列的C64x&#43;的DSP Cache配置,常见的Cache miss模式以及如何在多核或者多外设DMA系统下维护Cache一致性,当然还有Cache优化的专题,针对指令L1P和数据L1D cache的优化。
排名:千里之外
(1)(0)(6)(0)(6)(2)(0)(0)(10)(1)(0)(2)

我要回帖

更多关于 buff cache 太高 的文章

 

随机推荐