开发者分享| Vitis_HLS, 玩转AXI总线突发读写的代码风格-上

本文转载自: XILINX开发者社区微信公众号

本文作者:赛灵思产品应用工程师 Wen Chen

在Vitis HLS 工具中,要真正完成AXI总线突发,我们需要一个合适的代码风格并结合恰当的指令设置来达到这个目的。本章节带大家看看如何玩转AXI总线突发读写的代码风格-上。

突发读写的HLS 代码风格优化:

HLS编译器所能识别的突发读写有顺序突发(sequential burst) 和流水线突发(Pipeline burst),一般来说流水线突发的读写效率较为低下,大多数情况下可以通过优化代码风格实现高效的流水线突发读写。

1. HLS编译器是如何在代码中寻找并判断代码中的突发访问需求的?

(1)HLS 编译器会在函数的基本块中查找内存访问,例如在函数内部的一组顺序语句中查找内存访问。
(2)假设满足突发的先决条件,在这些基本块中推断出的每个突发称为顺序突发。编译器将自动扫描基本块以将最长的访问序列构建到单个顺序突发中。
(3)然后编译器查看循环并尝试推断所谓的流水线突发。

流水线突发一般是针对的是循环迭代中的读/写序列。编译器试图通过分析循环索引和循环的边界变量来推断突发读写的长度。如果成功解析一个可以突发读写的结构体,编译器可以将循环的每次迭代中的读/写序列链接成一个长流水线突发。

目前的Vitis HLS编译器会自动推断代码中是否存在有效的Sequential Burst 或者 Pipeline Burst,但没有办法专门请求顺序或流水线突发。也就是说设计者们目前需要遵循符合条件的代码风格让工具推断顺序或流水线突发。

2. 成功实现突发读写代码风格有哪些要求?

聚合连续的内存访问请求是能够突发读写关键。以下是这些连续访问必须满足的一组先决条件:
(1)必须全部读取,或全部写入 - 突发读取和写入是不可能的。
(2)必须是单调递增的访问顺序(在访问的内存位置和时间方面)。总线无法访问位于两个先前访问过的内存位置之间的内存位置。
(3)在内存中必须是连续的——一个紧挨着另一个,没有间隙或重叠,并且是向前的。
(4)在发出请求之前,必须确定读/写访问的次数(或突发长度)。这意味着即使突发长度是参数化的,也必须在发送读/写请求之前进行计算。
(5)如果将两个数组捆绑到同一个 M-AXI 端口,则在任何给定时间的每个方向最多只对一个数组进行突发。
(6)从发起和完成突发请求的时间开始,不得存在数据依赖性的问题。

3. 从实际代码出发,看看不同的代码风格下编译器如何分别流水线突发和顺序突发读写

以下代码是一个非常典型的从一个数组中执行一系列读取,以及从循环内再对另一个数组进行一系列写入。

for(size_t i = 0; i

out[4*i+0] = f(in[4*i+0]);

out[4*i+1] = f(in[4*i+1]);

out[4*i+2] = f(in[4*i+2]);

out[4*i+3] = f(in[4*i+3]);

}

如果编译器可以成功地从循环的边界变量(size)和循环的行程计数推导出突发长度,它将推断出一个大流水线突发并将 ReadReq、WriteReq 和 WriteResp 移动到循环外,如下图的流水线突发伪代码所示。因此,所有循环迭代的读请求合并为一个读请求,所有写请求合并为一个写请求。这里要注意的是,所有读取请求通常会立即发出,而写入请求仅在数据可用后才会发出。

/* requests can move anywhere in func */

rb = ReadReq(in, size);

wb = WriteReq(out, size);

for(size_t i = 0; i

Write(wb, 4*i+0) = f(Read(rb, 4*i+0));

Write(wb, 4*i+1) = f(Read(rb, 4*i+1));

Write(wb, 4*i+2) = f(Read(rb, 4*i+2));

Write(wb, 4*i+3) = f(Read(rb, 4*i+3));

}

WriteResp(wb);

但是,如果不满足任意一个上文所提到的突发传输的先决条件,编译器可能不会推断流水线突发,而是会尝试推断顺序突发,其中 ReadReq、WriteReg 和 WriteResp 位于读/写访问被突发优化,如顺序突发代码下面的伪代码片。在这种情况下,每次循环迭代的读和写请求都在一个循环内合并为一个读或写请求。很显然,顺序突发一次只能聚合4个读写需求,所以效率是低下的。

 for(size_t i = 0; i

rb = ReadReq(in+4*i, 4);

wb = WriteReq(out+4*i, 4);

Write(wb, 0) = f(Read(rb, 0));

Write(wb, 1) = f(Read(rb, 1));

Write(wb, 2) = f(Read(rb, 2));

Write(wb, 3) = f(Read(rb, 3));

WriteResp(wb);

}

提示:突发请求的大小可以进一步划分为用户指定大小的多个请求,这些请求使用 INTERFACE pragma 或指令的 max_read_burst_length 和 max_write_burst_length 进行控制,会在以后的文章中详细叙述。

本文我们了解到尽可能地促成流水线突发是提升吞吐率的关键操作,下一篇文章将例举几个典型的会导致编译器流水线突发推理(Burst inferencing)失败的情况,看看我们是否有可能在算法本身或者代码风格上规避这些情况。

最新文章