This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

C66代码优化基本介绍(二)

C66代码优化基本介绍(一) http://www.deyisupport.com/question_answer/f/53/t/14659.aspx

消除循环体间的相关性

再看一下汇编文件中的软件流水信息。瓶颈在哪里?要找出瓶颈,必须理解编译器是如何计算循环指令周期数的下限的。此下限是“循环体间的相关性限制”和“资源限制”的最大者。循环体间的相关性的区间值是以汇编指令间的执行顺序为基础的。例如,必须完成两个 load 指令后才能继续执行 add 指令。资源限制则是基于硬件资源,例如需要的每种指定类型的功能单元的数量。实际上,有以下两种资源限制:已划分和未划分。在本例中,这两者相同。

在本例中,已划分资源限制为 2。即使可以不按顺序执行汇编指令,执行循环体中的所有指令也至少需要2个周期。但是,循环体间的相关性限制为 7。

;*      Loop Carried Dependency Bound(^) : 7
;*      Unpartitioned Resource Bound     : 2
;*      Partitioned Resource Bound(*)    : 2

因此,ii ≥ max(2,7)。要减小 ii,进而减少指令周期数/结果,必须减小循环体间相关性限制。

循环体间相关性限制之所以增大,是因为在指令的数据相关图中有一个环。最大环的长度为循环体间相关性限制。为了减少或消除循环体间相关性,必须找到这个环并找出缩短或消除它的方法。

要确定最大循环体间相关性,请参阅单个循环体中的指令。关键环中涉及的指令标有 (^) 号。这些指令包含两个 load 指令以及 add 指令和 store 指令。

;*        SINGLE SCHEDULED ITERATION
;*
;*        C25:
;*   0              LDW     .D1T1   *A4++,A3          ; |6|  ^
;*     ||           LDW     .D2T2   *B4++,B5          ; |6|  ^
;*   1      [ B0]   BDEC    .S2     C24,B0            ; |5|
;*   2              NOP             3
;*   5              ADD     .L1X    B5,A3,A3          ; |6|  ^
;*   6              STW     .D1T1   A3,*A5++          ; |6|  ^
;*   7              ; BRANCHCC OCCURS {C25}           ; |5|

依靠这些信息以及查看指令间的相互供给关系,可以重新构建循环体间相关性的环。下图中的各节点正好是用 (^) 号表示的指令。各边(即节点对之间的箭头)表示排序约束。边上加注有从源指令和目标指令之间需要的指令周期数。在大多数情况下,结果在执行指令的指令周期末尾写入寄存器,并且可以在下一个指令周期才可使用。少数例外之一是,某些 load 指令需要 5 个周期才能使数据写入目标寄存器以供使用。

在此图中,有两个关键环,每个的长度都为 7。要减少循环传递相关性限制,必须缩短或消除图中最大的环。这可通过消除环中的一条边来实现。为此,必须了解边的来源。

从 load 指令到 add 指令的边是很好理解的。load 指令的目的寄存器是 add 指令的源寄存器。load 指令需要 5 个指令周期来填充其目的寄存器。因此,只有在执行完两个 load 指令中的最后一个 load 指令并等待 5 个指令周期结束后才能执行 add 指令。

从 add 指令到 store 指令的边也是很好理解的,因为 add 指令的目的寄存器是 store 指令的源寄存器。add 指令的结果在 1 个指令周期后可供使用。因此,add 指令和 store 指令之间的边加注有数字 1。store 指令可以在紧跟着 add 指令的指令周期中执行。

从 store 指令回到 load 指令的边就没有这么明显了。怎样知道要将它们加进来?为什么它们在那里?这些问题的答案可由消除过程来确定。因为没有寄存器相关性,最有可能存在存储器相关性。在本例中,编译器不知道输入数组是否会引用与输出数组相同的内存位置,因此保守起见就假定这是有可能的。从 store 指令到 load 指令的边可确保先执行一个循环体中的 store 指令再执行下一个循环体中的 load 指令,以便当这些 load 指令尝试读取由 store 指令写入的数据时也是正确的。实际上是否发生这种情况取决于运行时“input1”、“input2”和“output”的值。(有关数据相关性和用来表示它们的图形的更多背景知识,请参阅 Memory Alias Disambiguation on the TMS320C6000(TMS320C6000 的内存别名消除歧义)。)

在现实中,有经验的程序员通常这样编写代码,输入参数的数组和输出参数的数组是独立的。原因是,这会使算法更加并行,从而带来更好的性能。假定在所有调用点,“input1”或“input2”都不会访问与“output”相同的内存地址。将此信息告知编译器,从 store 指令到 load 指令的返回边将会消失。这可通过使用 -mt 选项或使用 restrict 关键字来实现。

void BasicLoop(int *restrict output,
int *restrict input1, 
int *restrict input2, 
int n)
{
int i;
#pragma MUST_ITERATE(1)
for (i=0; i<n; i++)
output[i] = input1[i] + input2[i];
}

尽管对于本示例而言使用 restrict 关键字界定两个 load 指令或一个 store 指令就已经足够,仍然建议使用 restrict 关键字界定能够用其界定的所有参数(以及本地指针变量)。首先,这通常比确定哪些参数真的需要用 restrict 关键字来界定更迅速。其次,这可以向将来可能维护或修改此代码库的其他程序员提供信息。但是,在插入 restrict 关键字之前,请确保使用 restrict 关键字界定的指针不能与任何其他指针重叠。当编写库程序并使用 restrict 关键字时,请务必为库用户用文档说明参数限制。

在添加 restrict 关键字之后,重新变异这个函数。请注意,循环传递相关性限制已消失。这意味着每个循环体都是独立的。现在,新的循环在资源许可时立即启动。

;*      Loop Carried Dependency Bound(^) : 0

需进一步注意,每 2 个周期会启动一个新的循环。因此,在稳定状态下,每 2 个周期(而不是每 7 个周期)就会生成一个新结果。

;*         ii = 2  Schedule found with 4 iterations in parallel.