Please note as of Wednesday, August 15th, 2018 this wiki has been set to read only. If you are a TI Employee and require Edit ability please contact x0211426 from the company directory.

C6000 Compiler: Tuning Software Pipelined Loops/zh

From Texas Instruments Wiki
Jump to: navigation, search

简介

C6000系列处理器的闪光之处就是它可以通过循环提高运行速度。这在以循环为中心的数字信号处理、图像处理和其他数学程序方面有着非常明显的优势。一种名为“软件流水”的技术对提高循环代码的性能做出的贡献最大。软件流水只有在使用-o2 或 -o3 编译选项时,才会被启用。对于除 C64x+ 以外的所有 C6000 型号,当使用优化代码大小的编译选项 -ms2 或 -ms3(参阅 C6000 Compiler:Recommended Compiler Options(C6000 编译器:建议的编译器选项))时,软件流水将被完全关闭。对于C64x+,当且仅当可以使用循环缓冲器(请参阅 Hand Tuning Loops and Control Code on the TMS320C6000(手动优化 TMS320C6000 上的循环和控制代码)的 2.1 部分)时,软件流水才会被启用。

如果不使用软件流水,循环就会在循环体 i 完成后再开始循环体 i+1。软件流水技术允许循环体出现重叠。因此,只要能够保持正确性,即可在循环体 i 完成之前开始循环体 i+1。在通常情况下,使用软件流水调度技术与不使用软件流水调度技术相比,使用时计算机资源利用率会高很多。

在软件流水循环中,即便一个循环体可能需要 s 个周期才能完成,但每隔 ii 个指令周期会启动一个新的循环。

Spra666 figure2.JPG

在一个高效的软件流水循环中,其中 ii<s,ii 被称为“启动间隔”;它是启动循环体 i 和启动循环体 i+1 之间的指令周期数。ii 等于软件流水循环体的指令周期数。s 是第一个循环体完成所需要的指令周期数,也是软件流水循环的“单个已调度的循环体”的长度。

在软件流水中,循环体有重叠,因此可能很难理解与原来循环相对应的汇编代码。如果源代码是使用 -mw 编译的,则软件流水循环信息会显示软件流水循环的单个循环体的指令调度顺序。仔细察看这个单一调度的循环体可使理解编译器的输出变得更加容易。接下来这又使调整循环优化变得容易。

警告:不要向编译器撒谎!

编译器拥有的信息越多,做出的优化决策就越好。使用注释时,确保所交流的信息是正确的。如果信息不正确,生成的代码也会不正确。

建立基准

假定有以下函数 BasicLoop() ...

void BasicLoop(int *output, int *input1, int *input2, int n)
{
int i;
for (i=0; i&lt;n; i++)
output[i] = input1[i] + input2[i];
}

使用建议的选项进行编译:-o -s -mw -mv6400。

打开汇编文件并查看此循环的软件流水信息:

;*   SOFTWARE PIPELINE INFORMATION
;*
;*      Loop source line                 : 5
;*      Loop opening brace source line   : 5
;*      Loop closing brace source line   : 6
;*      Known Minimum Trip Count         : 1
;*      Known Max Trip Count Factor      : 1
;*      Loop Carried Dependency Bound(^) : 7
;*      Unpartitioned Resource Bound     : 2
;*      Partitioned Resource Bound(*)    : 2
;*      Resource Partition:
;*                                A-side   B-side
;*      .L units                     0        0
;*      .S units                     0        1
;*      .D units                     2*       1
;*      .M units                     0        0
;*      .X cross paths               1        0
;*      .T address paths             2*       1
;*      Long read paths              0        0
;*      Long write paths             0        0
;*      Logical  ops (.LS)           0        0     (.L or .S unit)
;*      Addition ops (.LSD)          1        0     (.L or .S or .D unit)
;*      Bound(.L .S .LS)             0        1
;*      Bound(.L .S .D .LS .LSD)     1        1
;*
;*      Searching for software pipeline schedule at ...
;*         ii = 7  Schedule found with 1 iterations in parallel
...
;*        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|
;*----------------------------------------------------------------------------*
L1:    ; PD LOOP PROLOG
;** --------------------------------------------------------------------------*
L2:    ; PIPED LOOP KERNEL
LDW     .D1T1   *A4++,A3          ; |6| <0,0>  ^
||         LDW     .D2T2   *B4++,B5          ; |6| <0,0>  ^

[ B0]   BDEC    .S2     L2,B0             ; |5| <0,1>
NOP             3
ADD     .L1X    B5,A3,A3          ; |6| <0,5>  ^
STW     .D1T1   A3,*A5++          ; |6| <0,6>  ^
;** --------------------------------------------------------------------------*
L3:    ; PIPED LOOP EPILOG
;** --------------------------------------------------------------------------*

软件流水循环信息包括循环开始的源代码行、对循环资源和延迟要求的描述以及循环是否已展开(还有其他信息)。使用 -mw 编译时,该信息还包含单个已调度循环体的副本。C6000 Programmer's Guide(C6000 程序员指南)的第 4 章详细介绍了 -mw 注释块。尽管此信息不是最新的,但是在编写本文时它是可以获得的最好信息。

启动间隔 (ii) 为 7。这意味着在稳定状态,每 7 个 CPU 周期计算出一次结果(相当于原始循环)。因此,基准 CPU 性能为 7 个指令周期/结果。

可以看出,单个已调度循环体的长度也为 7。因此,在任何指定时间只执行一次循环,这样就没有循环体重叠(通常没有经过优化)。

消除循环体间的相关性

再看一下汇编文件中的软件流水信息。瓶颈在哪里?要找出瓶颈,必须理解编译器是如何计算循环指令周期数的下限的。此下限是“循环体间的相关性限制”和“资源限制”的最大者。循环体间的相关性的区间值是以汇编指令间的执行顺序为基础的。例如,必须完成两个 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 个周期才能使数据写入目标寄存器以供使用。

Spra666 figure3.JPG

在此图中,有两个关键环,每个的长度都为 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.

平衡资源

看一下词循环的软件流水信息。现在,资源限制指出约束的原因是功能单元的数量:

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

哪个功能单元是瓶颈?要确定瓶颈,可查看执行单个循环所需的功能单元的详细列表。请记住,C6000 体系结构分为两个几乎对称的部分。在软件流水信息中显示的资源列表是在编译器已经将指令划分到 A 侧或 B 侧之后计算的。

看一下后面标有 (*) 号的计算机资源。请注意哪些资源最拥堵。在本例中,瓶颈在 D 单元和 T 地址路径上。

;*      Resource Partition:
;*                                A-side   B-side
;*      .L units                     0        0
;*      .S units                     0        1
;*      .D units                     2*       1
;*      .M units                     0        0
;*      .X cross paths               1        0
;*      .T address paths             2*       1
;*      Long read paths              0        0
;*      Long write paths             0        0
;*      Logical  ops (.LS)           0        0     (.L or .S unit)
;*      Addition ops (.LSD)          1        0     (.L or .S or .D unit)
;*      Bound(.L .S .LS)             0        1
;*      Bound(.L .S .D .LS .LSD)     1        1
;*
;*      Searching for software pipeline schedule at ...
;*         ii = 2  Schedule found with 4 iterations in parallel
...
;*        SINGLE SCHEDULED ITERATION
;*
;*        C26:
;*   0              LDW     .D1T1   *A5++,A4          ; |9|
;*   1              LDW     .D2T2   *B4++,B5          ; |9|
;*   2      [ B0]   BDEC    .S2     C26,B0            ; |8|
;*   3              NOP             3
;*   6              ADD     .L1X    B5,A4,A3          ; |9|
;*   7              STW     .D1T1   A3,*A6++          ; |9|
;*   8              ; BRANCHCC OCCURS {C26}           ; |8|

从单个已调度的循环可以看出,D 单元和 T 地址路径用于 load 指令和 store 指令,每个指令都使用一个 D 单元和一个 T 地址路径。有三个内存访问指令,但是每个循环中只有两个 D 单元和两个 T 地址路径。因此,资源限制为两个周期。这意味着,在稳定状态下每个结果至少需要两个周期。

可以通过更好地利用 D 单元和 T 地址路径来提高性能。假定知道循环次数始终是偶数。如果将循环展开 2 次(因此生成的循环包含原始循环体的两个副本并且执行一半数量的循环),编译器可以平衡 D 单元和 T 地址路径并实现更好的资源利用率。

下图显示了展开循环以更好(更加平衡)地利用关键 D 单元资源的概念(情形与关键 T 地址路径类似)。左侧显示了四个循环体,用双箭头表示,在四个指令周期中生成八个结果。每隔一个指令周期 D 单元便有一次空闲。右侧显示了将循环展开 2 倍后的性能。在每个指令周期中两个 D 单元都执行有用的指令。当然,必须重新安排 load 指令和 store 指令的顺序,但是这由编译器来进行。

Spra666 figure4.JPG

实现此目的的一种可能方法是手动展开循环:

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+=2) {
output[i]   = input1[i]   + input2[i];
output[i+1] = input1[i+1] + input2[i+1];
    }
}

重新编译会产生以下结果:

;*   SOFTWARE PIPELINE INFORMATION
;*
;*      Loop source line                 : 8
;*      Loop opening brace source line   : 8
;*      Loop closing brace source line   : 11
;*      Known Minimum Trip Count         : 1
;*      Known Max Trip Count Factor      : 1
;*      Loop Carried Dependency Bound(^) : 0
;*      Unpartitioned Resource Bound     : 3
;*      Partitioned Resource Bound(*)    : 3
;*      Resource Partition:
;*                                A-side   B-side
;*      .L units                     0        0
;*      .S units                     0        1
;*      .D units                     3*       2
;*      .M units                     0        0
;*      .X cross paths               2        0
;*      .T address paths             3*       3*
;*      Long read paths              0        0
;*      Long write paths             0        0
;*      Logical  ops (.LS)           0        0     (.L or .S unit)
;*      Addition ops (.LSD)          2        0     (.L or .S or .D unit)
;*      Bound(.L .S .LS)             0        1
;*      Bound(.L .S .D .LS .LSD)     2        1
;*
;*      Searching for software pipeline schedule at ...
;*         ii = 3  Schedule found with 4 iterations in parallel
...
;*        SINGLE SCHEDULED ITERATION
;*
;*        C26:
;*   0              LDW     .D2T2   *B5++(8),B6       ; |10|
;*   1              NOP             1
;*   2              LDW     .D1T1   *A6++(8),A3       ; |10|
;*     ||           LDW     .D2T2   *-B5(4),B4        ; |10|
;*   3              LDW     .D1T1   *-A6(4),A3        ; |10|
;*   4              NOP             1
;*   5      [ B0]   BDEC    .S2     C26,B0            ; |8|
;*   6              NOP             1
;*   7              ADD     .S1X    B6,A3,A4          ; |10|
;*   8              ADD     .L1X    B4,A3,A5          ; |10|
;*   9              NOP             1
;*  10              STNDW   .D1T1   A5:A4,*A7++(8)    ; |10|
;*  11              ; BRANCHCC OCCURS {C26}           ; |8|


现在,T 地址路径已平衡。这比预期少一个 D 单元,因为编译器选择使用一个非对齐双字 store 指令而非两个对齐单字 store 指令。记住,非对齐内存访问使用两个 T 地址路径,但只使用一个 D 单元。

当循环体展开 2 倍时,每个循环所需的时间更长,但是现在循环在每个循环中生成两个结果而非一个。因此,在稳定状态下展开的循环需要 1.5 个指令周期/结果,而非展开形式需要 2 个指令周期/结果。

尽管手动展开循环会实现所需的结果,但是对大循环而言,这可能太费时耗力了。一个备选方法是让编译器进行此操作。如果编译器知道循环运行次数(在本例中为“n”)是 2 的倍数,则编译器会自动展开循环(如果认为有益)。要告知编译器运行次数为 2 的倍数,请修改循环前面的 MUST_ITERATE pragma。MUST_ITERATE pragma 的语法如下:

#pragma MUST_ITERATE(lower_bound, upper_bound, factor)

下限是“n”的最小可能值。上限是“n”的最大可能值。展开次数是一个始终能整除“n”的数。任何这些参数都可以被省略。

如果不手动展开循环,只需修改 pragma:

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

现在重新编译。吞吐量与手动展开循环时相同,即 1.5 个周期/结果。软件流水信息中的另外一行会告知编译器循环展开了 2 倍:

;*      Loop Unroll Multiple             : 2x

如果编译器认为展开循环无益(尽管使用了 MUST_ITERATE pragma),它就会不展开循环。这时,用户可以通过插入 UNROLL pragma(除使用 MUST_ITERATE pragma 以外)来强制编译器展开循环:

#pragma UNROLL(2)

同样,如果编译器选择展开循环,但是用户则希望不展开循环,可以在循环前面加上

#pragma UNROLL(1)

编译器通常会成功地选择最佳展开次数。但有时候,通过手动选择的展开次数,可以比编译器做得更好。原因是编译器必须对展开次数(如果有)进行预先猜测(在高级优化阶段)。而软件流水则是在进行低级别优化时作的。此时,展开决定已不可逆转。相比之下,作为用户,您有机会试用各种展开次数并挑选出最好的一个。

循环 pragma 必须刚好出现在循环之前,中间不得夹有任何其他源代码。请注意,编译器可能忽略 UNROLL pragma,除非伴随有 MUST_ITERATE pragma。MUST_ITERATE pragma 必须注明运行次数可以被展开次数整除,并且最小运行次数至少大于或等于展开次数。

当使用 C64x+ 时,请注意过度展开问题。否则,循环可能变得太大,以至于编译器无法利用循环缓冲器(请参阅 Hand Tuning Loops and Control Code on the TMS320C6000(手动优化 TMS320C6000 上的循环和控制代码)的 2.1 部分)。循环缓冲器具有低功耗、较小的代码和性能上的优势,但是只能用于 ii 为 14 或更小并且单个调度循环长度为 48 或更小的循环。

利用宽 load 指令和 store 指令 (SIMD)

尽管已经实现了 4.7 倍的增速,但是还有可能做得更好。请注意,循环仍然在内存访问指令处出现瓶颈。因为内存访问指令已经平衡,展开更多并没有帮助。但是,可以做出两项改进:

  • 可以使用更宽的 load 指令来代替多个 load 指令,以减少 D 单元资源的数量。
  • 可以使用对齐内存访问指令来代替非对齐内存访问指令,以减少 T 地址路径的数量。

C64x 和 C64x+ 处理器支持对齐和非对齐双字 load 指令与 store 指令。(请记住,C67x 和 C67x+ 仅支持对齐双字 load 指令,不支持双字 store 指令。C62x 仅支持字宽 load 指令和 store 指令。)如果知道函数参数是双字对齐,则应使用双字对齐内存访问指令,以便节省 D 单元和 T 地址路径。

如何让编译器选择双字形式的内存访问指令?有两个方法:(1) 使用intrinsics函数,或 (2) 告知编译器内存访问是对齐的。第二种方法更简单,因此最好首先尝试该方法。

要告知编译器内存访问在双字(64 位)边界上对齐,请在函数内所需的循环之前使用 _nasserts():

_nassert((int) input1 % 8 == 0); // input1 is 64-bit aligned
_nassert((int) input2 % 8 == 0); // input2 is 64-bit aligned
_nassert((int) output % 8 == 0); // output is 64-bit aligned

_nassert() 指出指针在函数中 _nassert() 所在的位置处对齐。尽管为每个函数指定一次对齐信息通常就已经足够,但是仍然建议在每个所需的循环之前重新声明对齐信息。

如果数据未在双字边界上对齐,则可以使用 DATA_ALIGN pragma 强制进行这种对齐。_nasserts() 会声明程序中 _nassert() 所在位置处变量的值的相关信息。根据此信息,编译器通常可以得出程序中其他位置处变量的相关信息。但是,为了获得最佳性能,如果函数包含多个循环,最好在每个循环的入口处重复 _nasserts()。

重新生成。资源限制,进而是 ii,已经降为 2:

;*   SOFTWARE PIPELINE INFORMATION
;*
;*      Loop source line                 : 13
;*      Loop opening brace source line   : 13
;*      Loop closing brace source line   : 15
;*      Loop Unroll Multiple             : 2x
;*      Known Minimum Trip Count         : 1
;*      Known Max Trip Count Factor      : 1
;*      Loop Carried Dependency Bound(^) : 0
;*      Unpartitioned Resource Bound     : 2
;*      Partitioned Resource Bound(*)    : 2
;*      Resource Partition:
;*                                A-side   B-side
;*      .L units                     0        0
;*      .S units                     0        1
;*      .D units                     2*       1
;*      .M units                     0        0
;*      .X cross paths               2*       0
;*      .T address paths             2*       1
;*      Long read paths              0        0
;*      Long write paths             0        0
;*      Logical  ops (.LS)           0        0     (.L or .S unit)
;*      Addition ops (.LSD)          2        0     (.L or .S or .D unit)
;*      Bound(.L .S .LS)             0        1
;*      Bound(.L .S .D .LS .LSD)     2*       1
;*
;*      Searching for software pipeline schedule at ...
;*         ii = 2  Schedule found with 4 iterations in parallel
...
;*        SINGLE SCHEDULED ITERATION
;*
;*        C26:
;*   0              LDDW    .D2T2   *B6++,B5:B4       ; |14|
;*     ||           LDDW    .D1T1   *A3++,A7:A6       ; |14|
;*   1              NOP             1
;*   2      [ B0]   BDEC    .S2     C26,B0            ; |13|
;*   3              NOP             2
;*   5              ADD     .S1X    B4,A6,A4          ; |14|
;*   6              ADD     .L1X    B5,A7,A5          ; |14|
;*   7              STDW    .D1T1   A5:A4,*A8++       ; |14|
;*   8              ; BRANCHCC OCCURS {C26}           ; |13|

在稳定状态下,循环现在每个指令周期生成一个结果(更精确地说,每两个周期生成两个结果),与基准相比速度快了 7 倍。

重新平衡资源

尽管采用更宽的 load 指令和 store 指令会加快循环的速度,但是循环仍然在 D 单元和 T 地址路径上出现瓶颈。内存访问次数已经降为 3。D 单元和 T 地址路径再次变得不平衡。与以前一样,可以通过再展开一次来实现进一步的改善(如果合法的话)。假定运行次数确实是 4 的倍数,修改 MUST_ITERATE pragma 以将此信息告知编译器。然后重新编译。生成的源代码(实现最佳吞吐量)如下所示:

void BasicLoop(int *restrict output,
int *restrict input1,
int *restrict input2,
int n)
{
int i;

_nassert((int) input1 % 8 == 0); // input1 is 8-byte aligned
_nassert((int) input2 % 8 == 0); // input2 is 8-byte aligned
_nassert((int) output % 8 == 0); // output is 8-byte aligned

#pragma MUST_ITERATE(4,,4)       // n >= 4, n % 4 = 0
for (i=0; i<n; i++) {
output[i]   = input1[i] + input2[i];
    }
}

生成的汇编代码的软件流水信息如下所示,该代码每 0.75 个周期生成一个结果(即每 3 个周期生成 4 个结果):

;*   SOFTWARE PIPELINE INFORMATION
;*
;*      Loop source line                 : 13
;*      Loop opening brace source line   : 13
;*      Loop closing brace source line   : 15
;*      Loop Unroll Multiple             : 4x
;*      Known Minimum Trip Count         : 1
;*      Known Max Trip Count Factor      : 1
;*      Loop Carried Dependency Bound(^) : 0
;*      Unpartitioned Resource Bound     : 3
;*      Partitioned Resource Bound(*)    : 3
;*      Resource Partition:
;*                                A-side   B-side
;*      .L units                     0        0
;*      .S units                     1        0
;*      .D units                     3*       3*
;*      .M units                     0        0
;*      .X cross paths               2        2
;*      .T address paths             3*       3*
;*      Long read paths              0        0
;*      Long write paths             0        0
;*      Logical  ops (.LS)           0        0     (.L or .S unit)
;*      Addition ops (.LSD)          2        2     (.L or .S or .D unit)
;*      Bound(.L .S .LS)             1        0
;*      Bound(.L .S .D .LS .LSD)     2        2
;*
;*      Searching for software pipeline schedule at ...
;*         ii = 3   Schedule found with 3 iterations in parallel
...
;*        SINGLE SCHEDULED ITERATION
;*
;*        C26:
;*   0              LDDW    .D2T2   *B5++(16),B9:B8   ; |14|
;*     ||           LDDW    .D1T1   *A16++(16),A7:A6  ; |14|
;*   1              LDDW    .D2T2   *-B5(8),B7:B6     ; |14|
;*     ||           LDDW    .D1T1   *-A16(8),A9:A8    ; |14|
;*   2              NOP             1
;*   3      [ A0]   BDEC    .S1     C26,A0            ; |13|
;*   4              NOP             1
;*   5              ADD     .S1X    B9,A7,A5          ; |14|
;*   6              ADD     .L1X    B8,A6,A4          ; |14|
;*     ||           ADD     .L2X    B6,A8,B6          ; |14|
;*   7              ADD     .L2X    B7,A9,B7          ; |14|
;*   8              STDW    .D1T1   A5:A4,*A3++(16)   ; |14|
;*     ||           STDW    .D2T2   B7:B6,*++B4(16)   ; |14|
;*   9              ; BRANCHCC OCCURS {C26}           ; |13|

与原始源代码相比,循环的速度提高了 9.3 倍,除添加 restrict 限定符、MUST_ITERATE pragma 和 _nasserts() 以外,并没有进行其他修改。

对于本示例,仅使用了三个 MUST_ITERATE pragma 字段中的两个。第三个(中间)字段,用于指定运行次数的上限,对于性能优化也很有用处。某些优化在循环运行次数较大时很有利,但是在运行次数较少时却会降低性能。默认情况下,编译器假定预期的运行次数较大。因此,如果运行次数的上限较小,最好将此信息告知编译器。

参考资料

本文来源于应用须知 Hand Tuning Loops and Control Code on the TMS320C6000(手动优化 TMS320C6000 上的循环和控制代码)