本文档适用于那些期望提高 C7000™ CPU 上运行的代码性能的用户。
本指南并不旨在帮助优化存储器/高速缓存层次结构、MSMC、DMA 或矩阵乘法加速器 (MMA) 的代码。
本文档的读者应具备以下能力:
作为本用户指南的补充,使用德州仪器 (TI) 提供的下述文档:
C7000™, C6000™, TMS320C6000™, and KeyStone™are TMs ofTexas Instruments.
Other TMs
在介绍可用来使代码更高效的编译器选项和源代码策略之前,有必要了解一些 C7000 数字信号处理器和指令集有关的一些信息。本章概要介绍了 C7000 架构、数据路径和功能单元。
C7000 CPU DSP 架构是德州仪器 (TI) 最新的高性能数字信号处理器 (DSP)。其配备在某些德州仪器 (TI) Keystone 3 器件中。这种超长指令字 (VLIW) DSP 因其宽矢量指令和多个功能单元而拥有显著的数学处理能力。本优化指南可以帮助开发人员实现 C7000 DSP 的最高性能。
当集成到更大的 TI 器件(例如一些 Keystone 3 器件)时,C7000 往往与矩阵乘法加速器 (MMA) 搭配使用,这样,可显著提高某些机器学习网络的性能。我们建议使用 TI 深度学习库,已对其进行经优以便使用矩阵乘法加速器。TI 深度学习库是 Processor(处理器) SDK 的一部分。
C7000 DSP 拥有矢量 (SIMD) 指令,根据数据类型和 C7000 CPU 版本,该指令能够在单条指令中执行多达 64 个操作。C7000 DSP 内核上几乎所有的计算指令都是完全流水线作业,这意味着在每个时钟周期均可启动独立指令。矢量指令与流水线行为的组合允许您在每个周期中进行大量的计算。C7000 DSP 内核具备定点和浮点矢量指令。
每个 C7000 DSP 内核都有若干功能单元。在每个时钟周期中,每个功能单元可以执行一条独立指令。在本指南中,重点介绍第一代 C7000 DSP 内核(C7100 和 C7120)。由于 C7100 和 C7120 DSP 内核有 13 个功能单元,因此每个时钟周期可执行 13 条指令。在现实中,一些功能单元专门用于执行某些类型的指令,因此出于这样或那样的原因,通常在每个周期中并非所有的 13 个功能单元都在执行指令。
有关 C7000 指令集的更多信息,请参阅 C71x DSP CPU、指令集和矩阵乘法加速器技术参考手册 (SPRUIP0)。
以下方框图显示 C7100 DSP CPU 上的分离式数据路径。图中有 A 端数据路径和 B 端数据路径。图中显示了功能单元和多个异构寄存器文件。A 端数据路径负责标量计算,从存储器加载和在存储器中存储标量与矢量,以及控制流(分支、调用)。B 端数据路径处理矢量数学运算、数据排列和矢量谓词运算。
为了简化上图,此图中未显示某些数据移动功能和数据路径。
C7100 和 C7120 器件具有 512 位向量宽度。C7504 和 C7524 器件具有 256 位向量宽度。寄存器每个寄存器有 64 位(“标量”)或每个寄存器有“向量宽度”位数。因此,C7100 和 C7120 器件具有 512 位向量寄存器,而 C7504 和 C7524 器件具有 256 位向量寄存器。
在给定的数据路径上,有若干不同类型的寄存器文件。在给定的数据路径上,每个功能单元都可以写入该数据路径上的全局寄存器文件以及该数据路径上的大部分“本地”寄存器文件。然而,只有一些功能单元可以从“本地”寄存器文件中读取。
除了提供对存储器层次结构进行 CPU 访问的 D1 和 D2 单元之外,C7100 DSP 还有两个“流引擎”,便于快速从存储器获取数据。流引擎 是一种硬件特性,允许您(或编译器)指定存储器地址模式,以便从存储器中获取数据。流引擎尽力将数据从存储器层次结构预取到靠近 CPU 的暂存存储器,以最大限度地减少由于冷缓存未命中而导致的 CPU 停顿。
本章介绍了德州仪器 (TI) C7000 C/C++ 编译器和可用于优化性能的选项。
德州仪器 (TI) C7000 编译器接受 C 或 C++ 源代码输入。编译时,编译器会经历几个阶段,如下图所示
首先,解析源文件,以创建高级中间表示,其与源语言非常相似,但更适合优化转换。
按某种优化级别编译的文件和函数(可选)通过高级优化器,该优化器执行函数内联、循环转换和其他代码优化。
然后,高级中间语言被翻译成与汇编非常相似的低级中间语言。低级优化器和代码生成传递执行分区、寄存器分配、软件流水线、指令调度和其他优化。
代码生成传递的输出是汇编文件,它由汇编器汇编成目标文件,然后由链接器链接到一个库或可执行文件中。
在应用经过全面调试并正常工作之后,就该开始优化过程了。首先,您需要选择适当的编译器选项。以下编译器选项会影响性能。有关命令行选项的更多详细信息,请参阅 C7000 C/C++ 编译器用户指南 (SPRUIG8)。
如果您关心性能,请勿使用 --disable_software_pipelining (-mu) 选项。此选项会关闭软件流水线。软件流水线对于在大多数循环上实现高性能至关重要。此选项可以作为调试工具,因为其使汇编代码更容易理解。
以下选项提供了用于调试和性能评估的附加信息:
--src_interlist (-s)。此选项使编译器在编译器生成的汇编文件中发送经过高级优化的源代码副本。此输出作为汇编代码中的注释置于汇编文件中。优化器的注释输出看起来像 C 代码,并显示了已应用的高级转换,例如内联、循环合并和矢量化。此选项有助于您理解汇编代码以及编译器为优化代码性能所做的一些工作。此选项会开启 --keep_asm (-k) 选项,因此编译器生成的汇编文件 (.asm) 将不会被删除。
--debug_software_pipeline (-mw)。此选项提供有关软件流水线循环的额外信息,包括循环的单次调度迭代。本文档后续介绍的循环调优示例中将使用此信息。此选项会开启 --keep_asm (-k) 选项,因此编译器生成的汇编文件 (.asm) 将不会被删除。
--gen_opt_info=2 (-on2)。此选项创建 .nfo 文件,其基本名称与 .obj 文件相同。此文件包含有关已应用的高级优化的摘要信息以及提供建议。
在解释汇编代码以及其内的软件流水线信息之前,了解一些编译器在将 C/C++ 源代码编译成汇编代码是试图对其执行的一些操作会有所帮助。
像 C7000 这样的超长指令字 (VLIW) 数字信号处理器 (DSP) 依靠循环的软件流水线实现性能最大化。软件流水线 是一种方法,其中源循环的连续迭代是重叠的,以便在整个循环中尽可能多的周期里利用 CPU 上的功能单元。
下图显示有软件流水线和无软件流水线的情况下循环迭代的执行。您可以看到,在没有软件流水线的情况下,循环被调试,因此循环迭代 i 完成之后迭代 i+1 才开始。在有软件流水线的情况下,迭代会重叠。因此,只要能够保持正确,即可在迭代 i 完成之前开始迭代 i+1。与其他方法相比,这种方法通常使机器资源的利用率更高。在软件流水线循环中,即使单个迭代可能需要 s 个周期才能完成,但每隔 ii 个周期就会启动一个新迭代。
在高效的软件流水线循环中,ii 远小于 s。ii 称为启动间隔,其是启动迭代 i 与启动迭代 i+1 之间的周期数。s 是完成第一个迭代所需的周期数,或者等于软件流水线循环的单次调度迭代的长度。
编译器尝试对最里面的源循环进行软件流水线处理。这些循环里面不再有任何其他循环。请注意,在编译过程中,软件流水线发生在内联之后和可将循环合并的循环转换之后,因此在某些情况下,您可能会看到编译器执行软件流水线的代码比预期的更多。
经过软件流水线处理后,循环有三个主要阶段,如下图所示:
以下示例显示了简单加权矢量和的源代码。
// weighted_vector_sum.cpp
// Compile with "cl7x -mv7100 --opt_level=3 --debug_software_pipeline
// --src_interlist --symdebug:none weighted_vector_sum.cpp"
void weighted_sum(int * restrict a, int *restrict b, int *restrict out,
int weight_a, int weight_b, int n)
{
#pragma UNROLL(1)
#pragma MUST_ITERATE(1024, ,32)
for (int i = 0; i < n; i++)
{
out[i] = a[i] * weight_a + b[i] * weight_b;
}
}
为了简化第一个软件流水线示例,使用了两个 pragma:
然后使用以下命令编译此代码:
cl7x --opt_level=3 --debug_software_pipeline --src_interlist --symdebug:none weighted_vector_sum.cpp
--symdebug:none
选项阻止编译器在汇编中生成调试信息和关联的调试指令。此调试信息与本文档的论述无关,如果包含该信息,则会不必要地延长此处所示的示例。一般不用关闭调试生成,因为调试信息的生成不会降低性能。
由于使用了 --src_interlist 选项,所以编译器生成的汇编文件未被删除,并包含以下内容:
;*----------------------------------------------------------------------------*
;* SOFTWARE PIPELINE INFORMATION
;*
;* Loop found in file : weighted_vector_sum.cpp
;* Loop source line : 10
;* Loop opening brace source line : 11
;* Loop closing brace source line : 13
;* Known Minimum Iteration Count : 1024
;* Known Max Iteration Count Factor : 32
;* Loop Carried Dependency Bound(^) : 0
;* Unpartitioned Resource Bound : 2
;* Partitioned Resource Bound : 2 (pre-sched)
;*
;* Searching for software pipeline schedule at ...
;* ii = 2 Schedule found with 7 iterations in parallel
;*
;* Partitioned Resource Bound(*) : 2 (post-sched)
. . .
;*----------------------------------------------------------------------------*
;* SINGLE SCHEDULED ITERATION
;*
;* ||$C$C36||:
;* 0 TICK ; [A_U]
;* 1 SLDW .D1 *D1++(4),BM0 ; [A_D1] |12|
;* || SLDW .D2 *D2++(4),BM1 ; [A_D2] |12|
;* 2 NOP 0x5 ; [A_B]
;* 7 MPYWW .N2 BM2,BM0,BL0 ; [B_N] |12|
;* || MPYWW .M2 BM3,BM1,BL1 ; [B_M2] |12|
;* 8 NOP 0x3 ; [A_B]
;* 11 ADDW .L2 BL1,BL0,B0 ; [B_L2] |12|
;* 12 STW .D1X B0,*D0++(4) ; [A_D1] |12|
;* || BNL .B1 ||$C$C36|| ; [A_B] |10|
;* 13 ; BRANCHCC OCCURS {||$C$C36||} ; [] |10|
;*----------------------------------------------------------------------------*
||$C$L1||: ; PIPED LOOP PROLOG
; EXCLUSIVE CPU CYCLES: 8
TICK ; [A_U] (R) (SP) <1,0>
|| SLDW .D1 *D1++(4),BM1 ; [A_D1] |12| (P) <1,1>
|| SLDW .D2 *D2++(4),BM0 ; [A_D2] |12| (P) <1,1>
MV .L2X A7,B0 ; [B_L2] |7| (R)
|| TICK ; [A_U] (P) <2,0>
MV .L2X A8,B1 ; [B_L2] |7| (R)
|| SLDW .D1 *D1++(4),BM0 ; [A_D1] |12| (P) <2,1>
|| SLDW .D2 *D2++(4),BM1 ; [A_D2] |12| (P) <2,1>
MV .S2 B0,BM2 ; [B_S2] (R)
|| MV .L2 B1,BM3 ; [B_L2] (R)
|| TICK ; [A_U] (P) <3,0>
MPYWW .N2 BM2,BM1,BL0 ; [B_N] |12| (P) <0,7>
|| MPYWW .M2 BM3,BM0,BL1 ; [B_M2] |12| (P) <0,7>
|| SLDW .D1 *D1++(4),BM0 ; [A_D1] |12| (P) <3,1>
|| SLDW .D2 *D2++(4),BM1 ; [A_D2] |12| (P) <3,1>
TICK ; [A_U] (P) <4,0>
MPYWW .N2 BM2,BM1,BL0 ; [B_N] |12| (P) <1,7>
|| MPYWW .M2 BM3,BM0,BL1 ; [B_M2] |12| (P) <1,7>
|| SLDW .D1 *D1++(4),BM0 ; [A_D1] |12| (P) <4,1>
|| SLDW .D2 *D2++(4),BM1 ; [A_D2] |12| (P) <4,1>
MV .D2 A6,D0 ; [A_D2] (R)
|| ADDD .D1 SP,0xfffffff8,SP ; [A_D1] (R)
|| TICK ; [A_U] (P) <5,0>
;** --------------------------------------------------------------------------*
||$C$L2||: ; PIPED LOOP KERNEL
; EXCLUSIVE CPU CYCLES: 2
ADDW .L2 BL1,BL0,B0 ; [B_L2] |12| <0,11>
|| MPYWW .N2 BM2,BM0,BL0 ; [B_N] |12| <2,7>
|| MPYWW .M2 BM3,BM1,BL1 ; [B_M2] |12| <2,7>
|| SLDW .D1 *D1++(4),BM0 ; [A_D1] |12| <5,1>
|| SLDW .D2 *D2++(4),BM1 ; [A_D2] |12| <5,1> BNL .B1 ||$C$L2|| ; [A_B] |10| <0,12>
|| STW .D1X B0,*D0++(4) ; [A_D1] |12| <0,12>
|| TICK ; [A_U] <6,0>
;** --------------------------------------------------------------------------*
||$C$L3||: ; PIPED LOOP EPILOG
; EXCLUSIVE CPU CYCLES: 7
;** ----------------------- return;
ADDD .D2 SP,0x8,SP ; [A_D2] (O)
|| LDD .D1 *SP(16),A9 ; [A_D1] (O)
|| ADDW .L2 BL1,BL0,B0 ; [B_L2] |12| (E) <4,11>
|| MPYWW .N2 BM2,BM0,BL1 ; [B_N] |12| (E) <6,7>
|| MPYWW .M2 BM3,BM1,BL0 ; [B_M2] |12| (E) <6,7>
STW .D1X B0,*D0++(4) ; [A_D1] |12| (E) <4,12>
ADDW .L2 BL1,BL0,B0 ; [B_L2] |12| (E) <5,11>
STW .D1X B0,*D0++(4) ; [A_D1] |12| (E) <5,12>
ADDW .L2 BL0,BL1,B0 ; [B_L2] |12| (E) <6,11>
STW .D1X B0,*D0++(4) ; [A_D1] |12| (E) <6,12>
RET .B1 ; [A_B] (O)
|| PROT ; [A_U] (E)
; RETURN OCCURS {RP} ; [] (O)
此汇编输出显示编译器生成的汇编文件中的软件流水线循环以及部分软件流水线信息注释块,其中包括关于各种循环特征的重要信息。
如果编译器成功地对循环进行软件流水线处理,那么编译器生成的汇编代码将包含软件流水线信息注释块,其中包含有关“ii = xx Schedule found with yy iterations in parallel”的消息。启动间隔 (ii
) 用于度量软件流水线循环开始执行循环新迭代的频率。启动间隔越小,执行整个循环所需的周期就越少。软件流水线循环信息还包括循环开始的源行、对循环资源和延迟要求的描述以及循环是否已展开(还有其他信息)。使用 –mw
编译时,该信息还包含单次调度迭代副本。
在本例中,实现的启动间隔 (ii) 是 2 个周期,并行运行的迭代次数为 7。
注释块还包括软件流水线循环的单次调度迭代 视图。从软件流水线循环的单次调度迭代视图中,可看到编译器如何转换代码以及编译器如何调度软件流水线循环的一次迭代与软件流水线中的迭代重叠。有关如何解释此注释块中的信息,请参阅节 5.2。
C7000 指令集具有许多功能强大的单指令多数据 (SIMD) 指令,可在一条指令中执行多个操作。为了利用这种优势,编译器会在可能而有益的情况下尝试对源代码进行矢量化。矢量化通常涉及使用矢量 (SIMD) 指令一次对数据的若干循环迭代执行操作。
以下示例从上一部分的示例中删除了 UNROLL pragma 和 MUST_ITERATE pragma。UNROLL(1) pragma 阻止了 C7000 编译器中的某些循环转换优化。
// weighted_vector_sum_v2.cpp
// Compile with "cl7x -mv7100 --opt_level=3 --debug_software_pipeline
// --src_interlist --symdebug:none weighted_vector_sum_v2.cpp"
void weighted_sum(int * restrict a, int *restrict b, int *restrict out,
int weight_a, int weight_b, int n)
{
for (int i = 0; i < n; i++) {
out[i] = a[i] * weight_a + b[i] * weight_b;
}
}
下面显示生成的已被矢量化的内部编译器代码。
编译器的矢量化可通过“+= 16
”地址增量和优化器临时变量名称中的“32x16
”(表示临时变量中有 16 个 32 位元素)来推断。
;*** -----------------------g3:
;*** 6 ----------------------- if ( !((d$1 == 1)&U$33) ) goto g5;
;*** 6 ----------------------- VP$25 = VP$24;
;*** -----------------------g5:
;*** 7 ----------------------- VP$20 = VP$25;
;*** 7 ----------------------- __vstore_pred_p_P64_S32(VP$20, &(*(packed int (*)<[16]>)U$47),
*(packed int (*)<[16]>)U$38*VRC$s32x16$001+*(packed int (*)<[16]>)U$42*VRC$s32x16$002);
;*** 6 ----------------------- U$38 += 16;
;*** 6 ----------------------- U$42 += 16;
;*** 6 ----------------------- U$47 += 16;
;*** 6 ----------------------- --d$1;
;*** 6 ----------------------- if ( L$1 = L$1-1 ) goto g3;
生成的汇编文件中获得的软件流水线信息块如下所示:
;* SOFTWARE PIPELINE INFORMATION
;*
;* Loop found in file : weighted_vector_sum_v2.cpp
;* Loop source line : 6
;* Loop opening brace source line : 6
;* Loop closing brace source line : 8
;* Loop Unroll Multiple : 16x
;* Known Minimum Iteration Count : 1
;* Known Max Iteration Count Factor : 1
;* Loop Carried Dependency Bound(^) : 1
;* Unpartitioned Resource Bound : 2
;* Partitioned Resource Bound : 2 (pre-sched)
;*
;* Searching for software pipeline schedule at ...
;* ii = 2 Schedule found with 7 iterations in parallel
...
;*----------------------------------------------------------------------------*
;* SINGLE SCHEDULED ITERATION
;*
;* ||$C$C41||:
;* 0 TICK ; [A_U]
;* 1 VLD16W .D1 *D0++(64),VBM0 ; [A_D1] |7| [SI]
;* 2 VLD16W .D1 *D1++(64),VBM0 ; [A_D1] |7| [SI]
;* 3 NOP 0x4 ; [A_B]
;* 7 VMPYWW .N2 VBM2,VBM0,VBL0 ; [B_N2] |7|
;* 8 VMPYWW .N2 VBM1,VBM0,VBL1 ; [B_N2] |7|
;* 9 CMPEQW .L1 AL0,0x1,D3 ; [A_L1] |6| ^
;* 10 ANDW .D2 D2,D3,AL1 ; [A_D2] |6|
;* || ADDW .L1 AL0,0xffffffff,AL0 ; [A_L1] |6| ^
;* 11 CMPEQW .S1 AL1,0,A0 ; [A_S1] |6|
;* 12 [!A0] MV .P2 P1,P0 ; [B_P] |6| CASE-1
;* || VADDW .L2 VBL1,VBL0,VB0 ; [B_L2] |7|
;* 13 VSTP16W .D2 P0,VB0,*A1(0) ; [A_D2] |7|
;* || ADDD .M1 A1,0x40,A1 ; [A_M1] |6| [C1]
;* || BNL .B1 ||$C$C41|| ; [A_B] |6|
;* 14 ; BRANCHCC OCCURS {||$C$C41||} ; [] |6|
本例比较了上一部分中的示例输出,以显示矢量化的效果:
-os
编译器选项时,此“优化器”代码显示在汇编文件中。)地址增量为 16,优化器临时变量的部分名称为“32x16”,表示 16 个 32 位元素。在此循环中,编译器不知道循环将执行多少次。所以在我们的示例中,如果循环迭代的次数不是所选矢量宽度中元素数量的倍数,编译器就不能在最后一次循环迭代中将整个矢量存储到存储器中。例如,如果原始(未矢量化的)循环将执行 40 次迭代,而编译器将循环矢量化 16 次,则最后一次优化迭代将计算 16 个元素,但其中只有 8 个应该存储到存储器中。
C7000 ISA 具有某些矢量谓词特性,其中矢量谓词会影响哪些通道应该执行矢量运算。在这种情况下,BITXPND 指令生成矢量谓词,将在感知矢量谓词的存储指令中使用。此矢量存储指令 (VSTP16W) 使用矢量谓词来防止将上次迭代中仅作为矢量化过程的结果计算且不会原始循环中计算或存储的那些元素存储到存储器中。编译器尝试在矢量化过程中自动执行矢量谓词。矢量谓词有助于避免生成剥离式循环迭代的需求,这种迭代可抑制循环嵌套优化。
如果使用 MUST_ITERATE pragma 向编译器提供有关循环迭代次数的信息,则可以避免进行向量预测。例如,如果已知上一个示例中的循环仅以 32 的倍数执行,并且最小迭代计数为 1024,则以下示例会改进生成的汇编代码:
// weighted_vector_sum_v3.cpp
// Compile with "cl7x -mv7100 --opt_level=3 --debug_software_pipeline
// --src_interlist --symdebug:none weighted_vector_sum_v3.cpp"
void weighted_sum(int * restrict a, int *restrict b, int *restrict out,
int weight_a, int weight_b, int n)
{
#pragma MUST_ITERATE(1024, ,32)
for (int i = 0; i < n; i++) {
out[i] = a[i] * weight_a + b[i] * weight_b;
}
}
在编译后,这个修改后的示例会生成以下软件流水线信息块:
;* SOFTWARE PIPELINE INFORMATION
;*
;* Loop found in file : weighted_vector_sum_v3.cpp
;* Loop source line : 7
;* Loop opening brace source line : 7
;* Loop closing brace source line : 9
;* Loop Unroll Multiple : 32x
;* Known Minimum Iteration Count : 32
;* Known Max Iteration Count Factor : 1
;* Loop Carried Dependency Bound(^) : 0
;* Unpartitioned Resource Bound : 4
;* Partitioned Resource Bound : 4 (pre-sched)
;*
;* Searching for software pipeline schedule at ...
;* ii = 4 Schedule found with 5 iterations in parallel
...
;*----------------------------------------------------------------------------*
;* SINGLE SCHEDULED ITERATION
;*
;* ||$C$C36||:
;* 0 TICK ; [A_U]
;* 1 VLD16W .D1 *D1++(128),VBM0 ; [A_D1] |8| [SI][C1]
;* 2 VLD16W .D1 *D1(-64),VBM0 ; [A_D1] |8| [C1]
;* 3 VLD16W .D1 *D2++(128),VBM0 ; [A_D1] |8| [SI][C1]
;* 4 VLD16W .D1 *D2(-64),VBM0 ; [A_D1] |8| [C1]
;* 5 NOP 0x2 ; [A_B]
;* 7 VMPYWW .N2 VBM2,VBM0,VBL1 ; [B_N2] |8|
;* 8 VMPYWW .N2 VBM2,VBM0,VBL0 ; [B_N2] |8|
;* 9 VMPYWW .N2 VBM1,VBM0,VBL2 ; [B_N2] |8|
;* 10 VMPYWW .N2 VBM1,VBM0,VBL1 ; [B_N2] |8|
;* 11 NOP 0x2 ; [A_B]
;* 13 VADDW .L2 VBL2,VBL1,VB0 ; [B_L2] |8|
;* 14 VST16W .D2 VB0,*D0(0) ; [A_D2] |8|
;* || VADDW .L2 VBL1,VBL0,VB0 ; [B_L2] |8|
;* 15 VST16W .D2 VB0,*D0(64) ; [A_D2] |8| [C0]
;* 16 ADDD .D2 D0,0x80,D0 ; [A_D2] |7| [C0]
;* || BNL .B1 ||$C$C36|| ; [A_B] |7|
;* 17 ; BRANCHCC OCCURS {||$C$C36||} ; [] |7|
由于添加了 MUST_ITERATE pragma,编译器知道绝不需要向量预测,并且不会执行向量预测。因此,编译器删除了 CMPEQW、ANDW、VSTP16W 以及与向量预测相关的其他指令。
如果 C7100 和 C7120 器件上使用了 --auto_stream=no_saving 选项,或 C7504 和更高版本的器件上使用了 --auto_stream=saving 选项,编译器可以自动使用流引擎 (SE) 和/或流地址生成器 (SA)。
如果节 3.3.2 中的 weighted_vector_sum_v3.cpp 示例是使用 --auto_stream=no_saving 选项编译的,则会生成以下软件流水线信息块。(本例中生成的汇编适用于 C7100。)
;* SOFTWARE PIPELINE INFORMATION
;*
;* Loop found in file : weighted_vector_sum_v2.cpp
;* Loop source line : 7
;* Loop opening brace source line : 7
;* Loop closing brace source line : 9
;* Loop Unroll Multiple : 32x
;* Known Minimum Iteration Count : 32
;* Known Max Iteration Count Factor : 1
;* Loop Carried Dependency Bound(^) : 2
;* Unpartitioned Resource Bound : 2
;* Partitioned Resource Bound : 2 (pre-sched)
;*
;* Searching for software pipeline schedule at ...
;* ii = 2 Schedule found with 4 iterations in parallel. . .
;*----------------------------------------------------------------------------*
;* SINGLE SCHEDULED ITERATION
;*
;* ||$C$C36||:
;* 0 TICK ; [A_U]
;* 1 VMPYWW .N2 VBM1,SE0++,VBL0 ; [B_N2] |8| ^
;* || VMPYWW .M2 VBM0,SE1++,VBL1 ; [B_M2] |8| ^
;* 2 VMPYWW .N2 VBM1,SE0++,VBL0 ; [B_N2] |8| ^
;* || VMPYWW .M2 VBM0,SE1++,VBL1 ; [B_M2] |8| ^
;* 3 NOP 0x2 ; [A_B]
;* 5 VADDW .L2 VBL1,VBL0,VB0 ; [B_L2] |8|
;* 6 VST16W .D2 VB0,*D0(0) ; [A_D2] |8|
;* || VADDW .L2 VBL1,VBL0,VB0 ; [B_L2] |8|
;* 7 VST16W .D2 VB0,*D0(64) ; [A_D2] |8| [C0]
;* || ADDD .D1 D0,0x80,D0 ; [A_D1] |7| [C1]
;* || BNL .B1 ||$C$C36|| ; [A_B] |7|
;* 8 ; BRANCHCC OCCURS {||$C$C36||} ; [] |7|
在本例中,编译器使用 SE0 和 SE1 来替换之前设置了更低 ii 下限 4 的负载。通过使用 SE 执行这些负载,可以实现 2 ii。要使用上述示例中的 SE,编译器必须配置和打开它们。配置和打开操作显示在由循环之前的 --src_interlist 选项添加的注释中:
;*** ----------------------- S$1 = __internal_SE_TEMPLATE_1_i_1_i_d_i_d_i_d_i_d_i_d_2_4;
;*** ----------------------- S$1.ICNT0 = C$5 = (unsigned)(n+15&0xfffffff0);
;*** ----------------------- __se_open_V0_U32_O(*__se_mem((packed void *)a), 0, S$1);
;*** ----------------------- S$3 = __internal_SE_TEMPLATE_1_i_1_i_d_i_d_i_d_i_d_i_d_2_4;
;*** ----------------------- S$3.ICNT0 = C$5;
;*** ----------------------- __se_open_V0_U32_O(*__se_mem((packed void *)b), 1, S$3);
默认情况下,编译器仅在使用 SE 或 SA 看起来有利可图且合法时才使用它们。
就盈利能力而言,一个关键的考虑因素是使用 SE 或 SA 会带来处理开销;编译器未必知道此开销有利可图。在示例中,MUST_ITERATE pragma 指示最小迭代计数为 1024,这使编译器相信使用 SE 或 SA 可能有利可图,因此编译器执行转换。如果编译器未使用 SE 或 SA,而您希望编译器使用它们,则使用 MUST_ITERATE 或 PROB_ITERATE pragma 指示迭代次数会有所帮助。
出于合法性的考虑,不使用 SE 或 SA 的大多数原因与是否需要寻址模式可始终映射到 SE 或 SA 相关。这些原因包括但不限于:
i
和 icnt
是 64 位类型时,for (i = 0; i < icnt; i++)
中会发生这种情况。dim
是 64 位类型时,data_in[i*dim]
中会发生这种情况。i
或 dim
是 64 位类型时,data_in[i*dim]
中会发生这种情况。int16_ptr[i]
中,当 int16_ptr
为 int16 *
且 i
为 int
时,最大范围是 INT_MIN*16 个元素到 INT_MAX*16 个元素。这些都是非典型情况,在实践中不太可能发生。要允许编译器忽略它们,请使用 --assume_addresses_ok_for_stream 选项。
如果使用 SE 或 SA 在实践中没有优势,您可以使用 FUNCTION_OPTIONS pragma 覆盖单个函数的 --auto_stream 和/或 --assume_addresses_ok_for_stream 选项。
如果代码在函数中显式使用 SE 或 SA,则编译器不会选择使用 SE 或 SA 进行优化。在这种情况下,编译器会假定代码使用该函数内的 SE 和 SA 处理优化的所有方面。
有关自动使用 SE 和 SA 以及相关编译器选项的详细信息,请参见 C7000 C/C++编译器用户指南 (SPRUIG8)。
如果折叠 或合并 嵌套循环合法而且可提高性能,那么编译器会尝试这么做。嵌套循环 是两个循环的集合,其中一个循环位于另一个封闭循环的内部。折叠和合并都涉及到将嵌套循环转换为单个循环。当外循环中没有代码时,就会发生折叠。当外循环中有代码时,就会发生合并。
两个嵌套循环组合成一个循环之后,必须转换外循环主体中的代码,从而仅在必要时有条件地执行该代码。折叠和合并都有利于性能,因为在执行循环嵌套时只执行一次 pipe-up (加速)和pipe-down(减速),若不执行循环合并/折叠,则每次执行外循环时都要执行内循环的pipe-up (加速)和 pipe-down(减速)。
要执行循环折叠或循环合并,合成的循环必须能够进行软件流水线作业。这意味着循环嵌套不能包含函数调用。每个循环必须有一个带符号的计数迭代器,每次迭代固定的次数。也就是说,内循环的迭代次数不能取决于外循环迭代的情况。此外,外循环不能包含太多代码,否则转换无法提高性能。如果外循环具有存储器依赖性,则可能不会执行循环合并和循环折叠。
当发生循环折叠或循环合并时,软件流水线循环会在软件信息注释块的顶部附近指示起始循环源代码行(“Loop source line
”)。当此源代码行号引用一个外循环时,这表示内循环已经完全展开,或者编译器已经执行循环合并或折叠。在循环合并的情况下,编译器使用特殊指令,例如 NLCINIT、TICK、GETP 和 BNL。这些硬件特性的说明(包括所谓的“NLC”)不在本文档的范围内。有关 NLC 的更多详细信息,可参阅 C71x DSP CPU、指令集和矩阵乘法加速器技术参考手册 (SPRUIP0)。
编译器有时采用头文件中定义的函数,并将代码放在调用位置。这样,允许在封闭式循环中形成软件流水线作业,从而提高性能。编译器也可以这样做来消除调用函数和从函数返回的成本。
在下例中,add_and_saturate_to_255()
函数对两个值求和,如果总和超过 255,则将总和限制为 255。此函数是从 inlining.cpp
中的函数调用的,该函数通过预处理器的 #include 指令包含 inlining.h
文件。
// inlining.cpp
// Compile with "cl7x -mv7100 --opt_level=3
// --debug_software_pipeline --src_interlist"
#include "inlining.h"
void saturated_vector_sum(int * restrict a, int * restrict b,
int * restrict out, int n)
{
#pragma MUST_ITERATE(1024,,)
#pragma UNROLL(1)
for (int i = 0; i < n; i++)
{
out[i] = add_and_saturate_to_255(a[i], b[i]);
}
}
// inlining.h
int add_and_saturate_to_255(int a, int b)
{
int sum = a + b;
if (sum > 255) sum = 255;
return sum;
在此例中,编译器将内联对 add_and_saturate_to_255()
的调用,因此可执行软件流水线作业。您可以通过查看生成的汇编文件底部确定是否执行了内联。此处,编译器放置入了注释,说明 add_and_saturate_to_255()
已内联。请注意,由于 C++ 名称已改编,故函数的标识符也已被修改。
;; Inlined function references:
;; [0] _Z23add_and_saturate_to_255ii
在生成的汇编代码中也可以看到内联,因为循环中没有针对函数的调用指令。事实上,由于内联(因而消除了对函数的调用),循环可以进行软件流水线。 如果在循环中调用了另一个函数,则不会发生软件流水线。 请注意,由于代码大小的问题,并非每个可内联的调用都会自动内联。有关内联的更多信息,请参阅 C7000 优化编译器用户指南。
;*----------------------------------------------------------------------------*
;* SINGLE SCHEDULED ITERATION
;*
;* ||$C$C44||:
;* 0 TICK ; [A_U]
;* 1 SLDW .D1 *D1++(4),BL0 ; [A_D1] |5|
;* 2 SLDW .D2 *D2++(4),BL1 ; [A_D2] |5|
;* 3 NOP 0x5 ; [A_B]
;* 8 ADDW .L2 BL1,BL0,BL1 ; [B_L2] |5|
;* 9 VMINW .L2 BL2,BL1,B0 ; [B_L2] |5|
;* 10 STW .D1X B0,*D0++(4) ; [A_D1] |5|
;* || BNL .B1 ||$C$C44|| ; [A_B] |11|
;* 11 ; BRANCHCC OCCURS {||$C$C44||} ; [] |11|
;*----------------------------------------------------------------------------*
为了对循环形成软件流水线(从而提高性能),循环中唯一可能出现的分支是返回到循环顶部的分支。if-then 和 if-then-else 语句的分支或其他控制流构造的分支将阻止软件流水线。
为了摆脱这种限制,编译器进行了 if-conversion。通过预测指令以便根据“if”语句中的测试有条件地执行指令,if-conversion 尝试删除 if-then 和 if-then-else 语句相关联的分支。只要 if-then 或 if-then-else 语句中没有太多嵌套级、太多条件项或太多指令,if-conversion 通常会成功。
下述示例演示了 if-conversion。为了对此 C++ 代码中的“for”循环形成软件流水线,必须进行 if-conversion。pragma 用于防止编译器矢量化和生成对本例不重要的附加代码。
// if_conversion.cpp
// Compile with "cl7x -mv7100 --opt_level=3 --debug_software_pipeline
// --src_interlist --symdebug:none if_conversion.cpp"
void function_1(int * restrict a, int *restrict b, int *restrict out, int n)
{
#pragma UNROLL(1)
#pragma MUST_ITERATE(1024, ,32)
for (int i = 0; i < n; i++)
{
int result;
if (a[i] < b[i])
result = a[i] + b[i];
else
result = 0;
out [i] = result;
}
}
编译后,软件流水线信息注释块中循环的单次调度迭代如下所示:
;*----------------------------------------------------------------------------*
;* SINGLE SCHEDULED ITERATION
;*
;* ||$C$C65||:
;* 0 TICK ; [A_U]
;* 1 SLDW .D1 *D2++(4),A1 ; [A_D1] |17| ^
;* || SLDW .D2 *D1++(4),A2 ; [A_D2] |17| ^
;* 2 NOP 0x5 ; [A_B]
;* 7 CMPGEW .L1 A2,A1,A0 ; [A_L1] |17| ^
;* 8 [!A0] ADDW .D2 A1,A2,D3 ; [A_D2] |17| ^
;* 9 [ A0] MVKU32 .S1 0,D3 ; [A_S1] |17|
;* 10 STW .D1 D3,*D0++(4) ; [A_D1] |17|
;* || BNL .B1 ||$C$C65|| ; [A_B] |9|
;* 11 ; BRANCHCC OCCURS {||$C$C65||} ; [] |9|
;*----------------------------------------------------------------------------
指令 [!A0] ADDW.D2 A1,A2,D3
代表 if 语句的“then”部分。指令 [A0] MVK32.S1 0,D3
代表 if 语句的“else”部分。CMPGEW 指令计算 if-condition 并将结果放入谓词寄存器,用于有条件地执行 ADDW 和 MVKU32 指令。
此部分讨论了基本的代码优化技术,该技术可应用于将在 C7000 DSP 内核上运行的 C/C++ 代码。