打印

关于TMS320C64x+ DSP-----Cache 优化

[复制链接]
610|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
Jasmines|  楼主 | 2017-11-2 10:43 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
关于TMS320C64x+ DSP-----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, tmp);   }      //++很显然需要增大tmp[],以保存func1每
for (i=0; i<N; i++)                          //++次循环的输出结果,作为func2的输入
{    function_2(tmp, out); }

>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 * x * h;
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   }
...
}

处理办法是在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   }
...
}


相关帖子

发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

745

主题

1077

帖子

10

粉丝