本文主要介绍TI C6000系列的C64x+的DSP Cache配置,常见的Cache miss模式以及如何在多核或者多外设DMA系统下维护Cache一致性,当然还有Cache优化的专题,针对指令L1P和数据L1D cache的优化。
C64x+与C64x CACHE的区别
Write Buffer(WB):
1. C64x+的 write buffe(WB)宽度为128-bit; 而C64x WB宽度为64-bit;
可cache性(Cacheability):
2. 对C64x+,外部内存地址通过MAR-bit配置其可cache性,只对L1D和L2有效,即对指令cache来说,只会cache到L1P级;但对C64x来说,外部内存地址配置其cache性后,对L1P,L1D和L2均有效;
3. 对C64x+,整个外部内存地址均可配置成可cache性;但对C64x,仅部分地址段可以;
Snooping protocol:
4. 对C64x+,其保持cache一致性机制更为有效,因为它直接将数据发送到L1D CACHE和DMA,消除了因invalidates导致的大量cache miss,而C64x通过invalid 和writeback cache line来维持一致性,效率显然低些;(C64x+在修改数据的同时就进行了一致性维护!)
5. C64x+的snoop 一致性协议不能维护L1P cache和L2 SRAM之间的一致性,这个工作需要程序员来完成,而C64x可以做到;
Cache 一致性操作:
6. 对C64x+,即使L2 cache被disable了,L2仍然总是根据L1P 和L1D的变化来对L2 进行cache一致性维护。对C64x则不是这样,它需要明确使用L1一致性操作指令来对对L2进行维护;
7. C64x+支持完全的L1D cache一致性操作,相对应,C64x仅支持L1D范围内的invalidate 和writeback-invalidate;
8. 当cache大小改变时,C64x+在初始化新尺寸cache时会自动writeback-invalidate cache,相反,C64x需要程序员发出明确的writeback-invalidate指令;
9. C64x+,L2 cache不包含(noninclusive)L1D和L1P。这意味从L2牺牲的行不会造成L1P和L1D的对应行也被牺牲。然而,对C64x却是如此。noninclusive的优势是由于取指在L2中分配的行不会牺牲L1D cache中的数据行,由于取数在L2中分配的行不会造成L1P中的指令行被牺牲掉。这有利于减少cache misses。
10. C64x+在L1和L2之间添加了一条高带宽的DMA通道(IDMA),用于有效的将数据从L1 SRAM搬进、搬出。(SPRU871 有IDMA的详细介绍)
11. 由于C64x+上miss会导致大量的延时stall,因此,消除miss和充分利用miss pipelining变得非常重要。对C64x+来说,有了L1 SRAM、比较大的L1 cache容量、low-overhead snooping和L2 cache的noninclusivity, 这点相应变得容易了。
12. 利用L1D的miss pipelining对改善性能非常的重要。对C64x,数据miss pipelining可以减少4个stall;对C64x+,数据miss pipelining可以减少7.5到9个stall circles.
13. 通过C64x+带宽管理,不同请求间的访问冲突和bank冲突得以解决。(带宽管理详见:TMS320C64x+ DSP Megamodule Reference Guide)。
14. C64x+ 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一致性
一、配置cache
>配置L1 Cache:
CACHE_L1pSetSize(); CACHE_L1dSetSize();
>配置L2 cache:
启动时默认状态下L2 cache被disabled, 全部L2为SRAM。如果启用了DSP/BIOS,则L2 cache被自动使能;否则,可以通过调用CSL命令:CACHE_L2SetSize()来使能L2 cache。
>外部内存的cacheability
L1D 和 L2可以通过调用CSL命令CACHE_enableCaching(CACHE_MARi)修改对应MAR-bit来控制外部内存段的cacheability; 而对L1P,外部内存总是cacheability的,跟MAR无关。
注意:配置成cache的地址段就不能再放入data and code,因为链接的时候,是不包含cache地址段的,如果要使用L1D SRAM或L1P SRAM,则应该相应减小cache段大小。
>example for C64x+ Linker Command File
MEMORY
{
L2SRAM : origin = 00800000h length = 001C0000h
CE0 : origin = 80000000h length = 01000000h (DDR第一个16M配置成可cache)
}
SECTIONS
{
.cinit > L2SRAM
.text > L2SRAM
.stack > L2SRAM
.bss > L2SRAM
.const > L2SRAM
.data > L2SRAM
.far > L2SRAM
.switch > L2SRAM
.sysmem > L2SRAM
.tables > L2SRAM
.cio > L2SRAM
.external > CE0
}
>example for C64x+ CSL Command Sequence to Enable Caching
[
#include <csl.h>
#include <csl_cache.h>
...
CACHE_enableCaching(CACHE_CE00);
CACHE_setL2Size(CACHE_256KCACHE);
]
二、cache一致性问题
如果内存是共享的,可以cache访问的,并且被修改了,那么这块内存就存在cache一致性维护的问题。对目前我们简单的编解码器移植来说,存在ARM与DSP的共享,但不存在同时修改的问题,故无需一致性维护。 对C64x+ DSPs来说,其cache 控制器可以根据snoop command以硬件cache一致性协议来自动维护由CPU EDMA/IDMA所访问数据的一致性。当DMA发起读和写命令时就激活了一致性维护机制。当DMA读L2 SRAM cache内存时,数据直接从L1D cache提交给DMA,而不会在L2 SRAM更新;当DMA写时,数据直接提交到L1D cache,并且更新L2 SRAM。
>除了一致性操作,对于DMA buffer,最好将其按L2 cacheline对齐,并保证其为cachelines size的整数倍,可以通过以下方式保证:
#pragma DATA_ALIGN(InBuffA, CACHE_L2_LINESIZE)
#pragma DATA_ALIGN(InBuffB, CACHE_L2_LINESIZE)
#pragma DATA_ALIGN(OutBuffA,CACHE_L2_LINESIZE)
#pragma DATA_ALIGN(OutBuffB,CACHE_L2_LINESIZE)
unsigned char InBuffA [N*CACHE_L2_LINESIZE];
unsigned char OutBuffA[N*CACHE_L2_LINESIZE];
unsigned char InBuffB [N*CACHE_L2_LINESIZE];
unsigned char OutBuffB[N*CACHE_L2_LINESIZE];
我们也可以调用CSL宏CACHE_ROUND_TO_LINESIZE(cache, element count,element size)定义数组来完成上述功能,第一个参数是cache类型,可以为L1D,L1P,L2:
unsigned char InBuffA [CACHE_ROUND_TO_LINESIZE(L2, N, sizeof(unsigned char)];
unsigned char OutBuffA[CACHE_ROUND_TO_LINESIZE(L2, N, sizeof(unsigned char)];
unsigned char InBuffB [CACHE_ROUND_TO_LINESIZE(L2, N, sizeof(unsigned char)];
unsigned char OutBuffB[CACHE_ROUND_TO_LINESIZE(L2, N, sizeof(unsigned char)];
>一行在L1D/L1P中cache到不一定在L2中也能cache到;一行可能从L2中牺牲掉但L1P/L1D 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 = 00800000h l = 00008000h /*1st 32K segment: always SRAM */
L2_2: o = 00808000h l = 00008000h /*2nd 32K segment:Task A-SRAM,Task B-Cache */
CE0 : o = 80000000h l = 01000000h /*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性能很大程度上依赖于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用于一般的类似控制流程等大量内存访问无法预测的部分。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+提供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++)
{ 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会严格按照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++)
{ function_1(in[i], tmp[i]); } //++很显然需要增大tmp[],以保存func1每
for (i=0; i<N; i++) //++次循环的输出结果,作为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类似,区别在于前者是2-way set-associative,而后者是direct-mapped,这意味着对L1D,两个数组能够映射到同一set,并同时保存在L1D。
@定义数组后,通过编译选项-m生成map file可以查看给该数组分配的地址。
与L1P类似,如果不连续定义数组,会导致各种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) //++ 数组按照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); //++如果w[],x[],h[]三个数组在内存中都映射到
_nassert((int)x % 8 == 0); //++同一L1D cache set,则L1D thrashing发生。当前读入
_nassert((int)h % 8 == 0); //++的w,x被随后读入的h给替换掉了....
#pragma MUST_ITERATE(16,,4)
for (i=0; i<N; i++)
sum += w[i] * x[i] * h[i];
return sum; }
处理办法是在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++) {
o = i * N/4;
dotprod(in1+o, w+o, N/4);
dotprod(in2+o, w+o, N/4);
dotprod(in3+o, w+o, N/4);
dotprod(in4+o, w+o, N/4); }
可以利用miss pipelining进一步减少read miss stalls.在每次循环开始用touch循环预先在cache分配w[],这样在每次调用点积函数前,需要的输入数组都准备好了:
for (i=0; i<4; i++) {
o = i * N/4;
touch(w+o, N/4 * sizeof(short));
touch(in1+o, N/4 * sizeof(short));
dotprod(in1+o, w+o, N/4);
touch(w+o, N/4 * sizeof(short)); //++每次touch in[]前都要touch w[]是为了保证w[]为MRU,
touch(in2+o, N/4 * sizeof(short)); //++以防访问顺序发生改变导致w[]被驱逐掉。
dotprod(in2+o, w+o, N/4);
touch(w+o, N/4 * sizeof(short));
touch(in3+o, N/4 * sizeof(short));
dotprod(in3+o, w+o, N/4);
touch(w+o, N/4 * sizeof(short));
touch(in4+o, N/4 * sizeof(short));
dotprod(in4+o, w+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) //++意味着已#pragma DATA_MEM_BANK(w, 0)
short w [N];
#pragma DATA_MEM_BANK(in1, 2) //++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 miss的。
void vecaddc(const short *restrict x, short c, short *restrict r, int nx)
{ int i;
for (i = 0 ; i < nx; i++)
r[i] = x[i] + c;
}
{
short in[4][N];
short out [N];
short ref [N];
short c, r;
for (i=0; i<4; i++)
{
vecaddc(in[i], c, out, N);
r = dotprod(out, ref, N);
}
}
C64x+ DSP 的带宽管理系统(BWM)
BWM的目的:确保某些请求(requestors)不会长时间的霸占总线,而阻止了来自c64x+ megamodule内源(resources)的请求。与c64x+的内存保护类似,BWM可对整个c64x+ megamodule做全局定义,但由每个local c64x+ megamodule源来实现。由此,初始化BWM就包含了对每个c64x+ 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+ 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+ 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),每个寄存器对应一个不同的请求。对应每个源的一组寄存器有相同的初始化值,对大多数应用来说,CPUARBD/ IDMAARBD/ SDMAARBD/ UCARBD的初始化值不需要修改。但MDMAARBE定义的优先级用于c64x+ megamodule以外数据传输,可能需要根据对应系统手册做些调整,大多数情况下,MDMAARBE应该通过编程提高优先级(赋更小值)。
开发过程中的其他问题:
1. IDMA:对达芬奇平台来说,感觉IDMA好像并不那么实用,首先,需要同时配置多个外部寄存器,如QDMA的情况并不多;其次,IDMA的一个主要功能是实现local mem之间的数据、代码搬移,可是达芬奇L2仅有64K,就算全部配置成SRAM,空间也是有限的,能够允许在此过渡的数据量也很有限;
2. 其他:DM6446的DMA是封装在FC里来协调算法和应用程序之间对DMA资源的控制的,对于底层实现,TI也提供了EDMA3LLD(low level driver)来实现DSP算法对EDMA资源的申请和使用。
Reference:
http://en.wikipedia.org/wiki/Texas_Instruments_DaVinci
http://en.wikipedia.org/wiki/Direct_memory_access
http://en.wikipedia.org/wiki/Cache_algorithms
http://www.blog.163.com/houh-1984/
本文主要介绍TI C6000系列的C64x+的DSP Cache配置,常见的Cache miss模式以及如何在多核或者多外设DMA系统下维护Cache一致性,当然还有Cache优化的专题,针对指令L1P和数据L1D cache的优化。