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

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

10.2. 优化循环控制

英特尔 Stratix 10 OpenCL设计利用FPGA的Hyperflex™架构来实现高性能。 因为Hyperflex™架构使得OpenCL设计运行得更快,因此优化英特尔 Stratix 10 OpenCL设计中的循环结构变得更为重要;否则,它们会导致显著的性能限制。

为了实现高性能, Intel® 建议实现循环启动间隔(II)为1。II值为1表示循环能够在每个时钟周期开始循环数据路径的新迭代。这样有助于您的设计有效使用可用的FPGA资源。

Intel® 已经建立了专门针对英特尔 Stratix 10架构的新的循环控制方案。

英特尔 Stratix 10 OpenCL设计中应用循环控制优化

采用英特尔 Stratix 10 Hyperflex架构,您现在可以在设计中创建深度流水线化循环来获得更高的fMAX。因为离线编译器可能无法在单个时钟周期内计算出如此复杂的循环结构的退出条件,但是现在离线编译器延迟了退出条件的完整计算。编译器将计算从循环体去耦,并将该计算拆分到多个时钟周期之中去。这样做使得在编译器完成计算退出条件之前,每个时钟周期启动循环迭代;但是,在发出循环退出条件信号后,需要几个时钟周来刷新循环。

请参阅High Level Design Report (report.html)中的Loop analysis report (循环分信报告)部分,找出离线编译器对哪些循环应用了新的循环优化策略。

新的循环控制优化策略的效果:

  1. 使得更快地启动当前循环迭代。
  2. 当前调用刷新所有数据后,后续循环调用开始。
注: 一次循环迭代是循环体的一次执行。循环调用是执行一次整个循环,从循环计数器的初始值直到退出条件变为TRUE

以下代码实例说明与嵌套循环相关的端接开销:

kernel loop_overhead(unsigned N) {
   for (unsigned int i = 0; i < N; i++) {
      for (unsigned int j = 0; j < N; j++) {
         //do work
         //total iterations: i * (j + s)
      }
   }
}

该循环的II值为1;然而,其发布嵌套中所有循环迭代所需要的时钟周期数是N × (N+s),其中s是在下几次迭代启动之前刷新循环需要的周期数。循环开销小;对设计就没有显著影响,除非设计的内部循环中的迭代次数非常少。

从循环控制优化受益的循环类型

大多数循环,即使是那些深度流水线化和具有复杂退出条件循环空的循环,都能够达到II值为1。该优化策略主要有利于具有多次循环迭代的高吞吐量设计。这些设计中fMAX的增加足以补偿相对较小的端接开销。

注: 循环控制流水线化的程度不影响循环的II值。

循环优化策略不适用于某些循环:

  • NDRange内核中的循环

    因为离线编译器必须能够流水线化循环,所以循环必须是单个work-item内核的一部分。

  • 具有退出条件的循环取决于指令是否能够在循环外停顿或者产生副作用。

以下是使用新循环控制方案的循环实例与不适用新循环控制方案的循环实例:

实例1:可以在英特尔 Stratix 10上实现的循环性能优化

kernel void good_loop(global int * restrict A, 
                      global int * restrict result,
					  unsigned N) {
  
   unsigned int sum = 0;
  
   for (unsigned int i = 0; i < N; i++) {
      sum += A[i];
   }
   *result = sum; 
}

实例2:可以在英特尔 Stratix 10上实现的循环性能优化

该实例中,循环外的通道写有副作用;但是,退出条件不依赖于通道写入。

channel unsigned int c0; 

kernel void producer() {
 
   for (unsigned int i = 0; i < 10; i++) {
      write_channel_intel(c0, i); 
   }   
}

实例3:循环无法充分从英特尔 Stratix 10循环控制方案受益,因为退出条件依赖于通道读取(read_channel_intel),这样可能在循环外产生副作用。因此,在编译器确定退出条件之前,无法继续进行每次迭代的计算,否则编译器必定消耗从通道来的额外数据。

kernel void consumer (global int * restrict A, 
                      global int * restrict result, 
                      unsigned N) {
   unsigned int sum = 0;
   for (unsigned int i = 0; 
        i < N && read_channel_intel(c0) != 5; i++) {
      sum += A[i];
   }
   *result = sum;
}

如果离线编译器未实现新的循环优化,还会禁用其它fMAX优化。

警告: 禁用这些优化可能会以牺牲fMAX未代价来减少逻辑使用量。查看离线编译器的HTML报告来验证编译器优化的结果。