针对C64x+的一些优化经验(转帖 shmily_liu )
本人做TI DSP优化过程中的一些积累,显得比较凌乱,但都是比较常用而且非常有必要了解的点,大家可以接在这后面补充自己的经验:
>. 同一EP包中不会出现两条指令使用同一功能单元。
使用同一交叉通道的2条指令不能并行.
>. 两条指令使用同一功能单元,不能在同一cycle内将结果写出,因为每个unit仅有一个写出port.
>. 两条指令使用同一乘法单元时,它们不能在同一指令周期内向register写出结果,理由同上,每个unit仅有一个到寄存器文件的写出port.
>. 每个EP,每条data path下,最多两个unit能够通过交叉path(1X, 2X)读取反向register file中的同一个操作数(假定两个units读取同一个操作数).
>. 对C64X/C64X+ CPU,当指令通过cross path读取一个cycle前被更新的寄存器时,总会产生一个cycle的延时,称之为stall, stall时,不需要插入NOP指令,由硬件自动完成插 入,但要注意,如果被cross path读取的寄存器中的数据是由Load指令更新,或更新时距超过一个cycle,则不会产生stall. 通过合理安排指令流程,尽量在寄存器操作数更新 后一个cycle之后再由cross path访问之,以避免stall。大部分情况下,TMS320C6000 C编译器和汇编优化器会自动完成指令调整以避免stall产生.
>. 在同一EP中,不允许两个load和store指令对同一寄存器文件中的同一源/目的进行操作。且地址寄存器必须与DA所使用的D unit出于同一侧.
>. T1,T2支持64-bit load/store, T1/T2与unit同出现在load/store指令中. 相对于C62x DSP,C64x有改进,后者的.D功能单元亦与交叉通道相联,也可每周期由交叉通道从另一 侧寄存器组读取一个源操作数;且允许一侧的多个功能单元由交叉通道从另一侧读数,只要是从另一侧的同一个源即可.
>. C64X/C64X+ dsp支持在任何byte边界使用非对齐的load/store访问word和doubleword。由此,word和doubleword数据均不需要32-bit或64-bit边界对齐.
L/S指令所用的地址指针寄存器必须与所用的.D功能单元处于同一侧
>. 当一unit做非对齐的Memory访问时(通常unit带T1/T2)时,不允许另一unit同时做memory操作,但允许另一unit同时做非memory访问的操作.
>. C62x与C67x系列因为data path共享,故都限制了同时读和写的40-bit数据的数目,而C64x/C64x+ cpu因为每个Unit采用分开的data path,故不存在以上限制.
>. 除条件寄存器外,在同一cycle内,不允许对同一寄存器做4次以上的读操作.
>. 同一cycle内,两条指令不能向同一寄存器写入。具有同一dst寄存器的两条指令也可以被安排成并行,条件时它们不会在同一cycle内向dst写入.
>. 使用MVC指令向寻址模式寄存器AMR写入,如果其后紧跟指令LD,ST,ADDA或SUBA,且此4指令使用了A4-A7,B4-B7寄存器寻址,则会产生一个cycle的stall. 记住,当以 A4~A7,B4~B7为基址寄存器时,将按照AMR寄存器的内容选择线性寻址或循环寻址.
>. 同一EP中不允许出现两条产生多个NOP cycle的指令.
>. 循环寻址方式可用于非对齐的数据访问,采用循环寻址方式后,地址更新和内存访问均按byte方式进行,即与相当量的byte序列的寻址访问相同.
>. 非对齐循环buffer访问方式适用于对逻辑地址相连内存区域的循环寻址,当非对齐访问到循环buffer边界时,将能正确的从buffer的两端读取数据,实现边界处的无缝连接.
>. 对C64x,唯一限制是循环buffer的大小至少与被访问的数据尺寸相同。如果buffer比被访问的数据类型小,则非对齐访问将产生无法预知的错误;而对C64x+,循环buffer至少为32bytes.
>. 程序指令:
>#pragma MUST_ITERATE(N1,N2, N3)---->向编译器提供信息,三个参数意义如下:下面的循环至少执行N1次;下面的循环至多执行N2此;循环执行次数必是N3的整数倍,这 三个参数不必全有。这个信息对编译器使用软件流水技术非常重要.
>>_nassert(表达式)---->此内联函数本身不产生任何代码,其作用是告诉编译优化器,其括号内的表达式为真,因而隐含的提示编译优化器,某种优化可能会有效.
>>volatile: 使用此关键字后,即使使用-o3编译选项,编译器也不会对该变量的访问做任何优化,以保证程序执行结果正确,也因为此,不可多用它,一般以下情况要用:
@凡是2个线程共享的全局变量就需要使用volatile关键字; @凡是某个内存地址的内容随时可能被外部硬件改变就需要使用volatile关键字。
>>restrict: 用此关键词向编译器标明一个指针是指向一个特定对象的唯一的指针,以提高memory访问速度。
>>变量存取方式及far关键字:
C编译器支持两种内存模式:大头模式和小头模式。不同内存模式主要影响对.bss段中的变量的访问方式。凡是程序中定义的全局变量(在函数外定义的变量)和静态变量(用static关键字定义的变量)都被编译器分配在.bss段中。在小头模式下,要求.bss段小于32K,即上两种变量总和不可超过32K,此时编译器将页指针DP(寄存器B14)指向.bss段的起始,对变量采用直接寻址方式,1条指令就可加载一个变量;而在大头模式下,对.bss没有任何限制,编译器对变量采用寄存器间接寻址方式,需要3条指令加载一个变量。这样,但定义的量超过32k,而又希望用小头模式时,可以采用far关键词。对于大的数组定义,使用far关键字,这样,buffer不会占用.bss段的地址空间,而时被编译器分配到.far段,对于数组,一般是用DMA访问或通过软件流水访问,不会又存取速度问题。
>>link文件开头:
-c :指定运行时初始化全局/静态变量, 系统调用C初始化函数c_init完成;
-heap 0x2000 :设置堆的大小
-stack 0x4000 :设置栈的大小
>>.text: 编译器生成的代码段一般以.text为段名,所有其他段都可以看做是数据段.
>>.bss : 全局变量(函数外定义的变量)和静态变量(static定义的变量)分配在此段.
>>.stack: 一般的局部变量(函数内部定义的变量)或是使用寄存器,或是分配在此段.
>>.const: 常量和字符串在此段
>>.cinit: 变量初值表,在编译器生成程序时,会将C程序中初始化的全局/静态变量的初始值按一定的数据结构放在此段,然后由C初始化函数c_init读取此段,来初始化.bss段中的变量,可见给变量赋初值的工作仍然要自己完成!!!
>. 使用选项 -k ,令编译器保留.asm文件,因为编译器产生的.asm文件反馈了许多信息,理解这些信息,按它的提示修改C语言程序,对尽快优化代码有好处.
>. C64x具有双字读取与存储指令,增大了访问存储器的带宽;峰值带宽可达到每个指令周期128位;增加了无边界调整的存储器访问能力,在没有32位或64位边界调整的严格限制下,C64x可以在单周期内完成64位数据的读写操作.
>. 在一系列嵌套循环中,最内层的循环是唯一可以进行软件流水的循环。循环不能软件流水的诸因素:
>>软件流水循环可包含intrinsics,但不能包含函数调用.
>>在循环中不可以有条件终止、使循环提前退出的指令.
>>循环必须是递减计数形式且在0时终止。使用-o2 and -o3选项的一个原因就是将尽可能多的循环转换成递减计数的循环.
>>在循环体中修改循环计数,则这个循环不能转换成递减计数循环,因而也无法进行流水.
>>代码尺寸太大,需要的寄存器数目大于C64xx的64个,这时需要简化循环或将循环拆成几个小循环.
>>如果要求一个寄存器的生命太长,则这个代码不能进行流水.
>>如果循环内有复杂的条件代码,条件代码需要大于C64xx的6个条件寄存器数,则这个循环不能进行软件流水.
>. 一般而言,用与源操作数相同字长的数据类型来保存累加和是非常危险的。通常选择是在计算过程(循环)内用较长的数据类型保存和数,最后根据具体情况选取适当字长。
>. 转移指令有5个周期的延迟间隙(当前cycle n,到cycle n+6跳转完毕,并执行指令),即在转移指令进入流水线后,要再等5个周期才发生跳转。所以,转移指令后的5个指令指向包(EP)都进入了CPU流水线,并相继执行,在写汇编时要特别注意这一点.
>. !!!!!!!!为防止数据访问的bank冲突,优化办法是将算法中访问的2个数组定义在不同的bank中,具体做法是定义不同的数据段:L1_data1, L1_data2,并在cmd中将这两个段定位到不同的两段内存: L1_data1 > memory1, L1_data2 > memory2; 将函数中用到的两个数组分别放在不同的存储块(要求芯片有一个以上存储块blocks); 将一个数组的偶元素和奇元素的访问安排在同一循环周期内。
>. 绝对不能把做了软件流水编排后输出的汇编语言程序再次作为汇编优化器的源程序输入,否则将导致整个程序编排错误。线性汇编语言应按照指令的自然逻辑顺序"线性"的安放指令,由汇编优化器对它分析,做出优化安排.