Intel® FPGA SDK for OpenCL™ Pro Edition: 最佳实践实践指南

ID 683521
日期 9/26/2022
Public
文档目录

6.2. 单个Work-Item内核的良好设计实践

如果您的OpenCL™内核包含循环结构,请遵循 Intel® 建议的指导构建内核,并使得 Intel® FPGA SDK for OpenCL™ Offline Compiler能够对它们进行有效地分析。 当您指示离线编译器运行循环中的流水线并行执行时,结构良好的循环尤为重要。

避免指针别名

尽可能在指针变量中插入restrict关键字。指针变量中包含restrict关键字可以防止离线编译器在无冲突的读写之间创建不必要的存储器依赖性。考虑一个循环,其中每次迭代从一个数组读取数据,然后将数据写入同一物理存储器中的另一个数组。如果不在这些指针变量中包含restrict关键字,离线编译器就可能会假定这两个数组之间存在依赖关系,并因此提取较少的流水线并行性。

构建"Well-Formed"(结构良好)的循环

"well-formed"的循环有一个与整数捆绑进行比较的退出条件,并且每次迭代都有一个简单的归纳增量。在内核中包含"well-formed"循环可以提高性能,因为离线编译器可以有效地分析这些循环。

以下是一个"well-formed"循环的实例:

for (i = 0; i < N; i++) {
   //statements
}
重要: “Well-formed"嵌套循环也有助于最大化内核性能。

以下是一个"well-formed"嵌套循环结构实例:

for (i = 0; i < N; i++) {
   //statements
   for(j = 0; j < M; j++) {
      //statements
   }
}

最小化循环携带的依赖性

因为每个循环迭代读取之前迭代写入的数据,以下循环结构中产生循环携带依赖性。这样,直到前一次迭代的写操作完成后,才能继续执行每个读操作。循环携带依赖性的存在会减少可达到的流水线化并行度,从而降低组件性能。

for (int i = 0; i < N; i++) {
    A[i] = A[i - 1] + i;
}

离线编译器对循环执行静态存储器依赖性分析,以确定其可以实现的并行度。某些情况下,离线编译器可能假设两个数组访问之间存在存在依赖关系,因此会析取较少的流水线并行性。如果因为未知变量,又或者数组访问涉及复杂寻址而导致编译期间无法解决此依赖性,则离线编译器会假设循环携带依赖项。

为了最小化循环携带的依赖项,请尽可能遵循以下指导:

  • 避免指针算法。

    当内核通过解除引用从算法操作获得的指针值来访问数组时,编译器输出仅为次优。例如,避免以如下方式访问数组:

    for (int i = 0; i < N; i++) {
        int t = *(A++);
        *A = t;
    }
  • 引入简单的数组索引。

    避免如下复杂数组索引类型,因为离线编译器无法有效分析这些类型,从而导致次优的编译输出:

    • 数组中的非常量(Nonconstant)。

      例如,A[K + i],其中i是循环索引变量,而K是未知变量。

    • 同一下标位置有多个索引变量。

      例如,A[i + 2 × j],其中ij是双嵌套循环的循环索引变量。

      注: 离线编译器可以有效分析数组索引A[i][j],因为索引变量在不同的下标中。
    • 非线性索引。

      例如,A[i & C],其中i是循环索引变量而C是是常量或者非常数变量。

  • 内核中尽量使用带有常量绑定的循环。

    带有常量绑定的循环使得离线编译器有效执行范围分析。

避免复杂循环退出条件

离线编译器评估退出条件以确定后续迭代是否可以进入循环流水线。有时离线编译器需要存储器访问或者复杂操作来评估退出条件。这些情况下,后续迭代要等到评估完成才能启动,从而降低了整体循环性能。

将嵌套循环转换为单个循环

为了最大限度地提高性能,请尽可能将嵌套循环合并成单一形式。将嵌套循环重构成单循环可减少循环迭代之间的硬件空间布局和计算开销。

以下代码实例说明嵌套循环到但循环的转换:

嵌套循环 转换后的单循环
for (i = 0; i < N; i++) {
    //statements
    for (j = 0; j < M; j++) {
        //statements          
    }
    //statements
} 
for (i = 0; i < N*M; i++) {
    //statements
}

避免条件循环(Avoid Conditional Loops)

为了最大限度的提高性能,请避免声明条件循环。条件循环是条件声明内所声明的循环的元组(tuple),这样,预计将达到有且仅有一个循环。这些循环不能有效并行化并导致串化实现。

以下代码实例说明将条件循环转换为更优化的实现:
条件循环 转换后的循环
if (condition) {
   for (int i = 0; i < m; i++) {
      // statements
   }
}
else {
   for (int i = 0; i < m; i++) {
      // statements
   }
}
for (int i = 0; i < m; i++) {
   if (condition) {
      // statements
   }
   else {
     // statements
   }
}

在尽可能深的范围内声明变量

为了减少实现变量所需要的FPGA硬件资源,请在循环中使用变量之前先声明该变量。在尽可能最深的范围声明变量可最小化数据依赖性和硬件使用,因为离线编译器不需要为不使用变量的循环保留变量数据。

请参考如下实例:

int a[N];
for (int i = 0; i < m; ++i) {
    int b[N];
    for (int j = 0; j < n; ++j) {
        // statements
    }
}

实现数组a比实现数组b需要更多资源。为减少硬件的使用,请在内部循环以外声明数组a,除非有必要通过外部循环迭代来维护数据。

提示: 尽可能将最深范围中变量的全部值覆盖掉,还可以减少呈现该变量所需要的资源。