halide
什么是Halide
Halide是用C++作为宿主语言的一个图像处理相关的DSL(Domain Specified Language)语言,全称领域专用语言。主要的作用为在软硬层面上(与算法本身的设计无关)实现对算法的底层加速。
一句话来说,Halide大大节省了我们手动优化底层算法的时间,让我们只需要关注算法的设计。Halide 实现算法与底层优化。
Halide为什么可以优化算法
Halide的特点是其图像算法的计算的实现(Function和Expression)和这些计算在计算硬件单元上的调度(Schedule)是分离的,其调度以Function为单位。最终将整个图像算法转换为高效率的多层for循环,for循环的分部数据范围划分和数据加载都是由Halide来完成的,而且可以实现数据的加载和算法计算的Overlay,掩盖数据加载导致的延迟。Halide的Schedule可以由程序员来指定一些策略,指定硬件的buffer大小,缓冲线的相关设置,这样可以根据不同的计算硬件的特性来实现高效率的计算单元的调度,而图像算法的计算实现却不需要修改。
决定算法在某个硬件平台上执行时性能的“三角力量”如下。
其中,算法本身的设计是一方面,一个好的算法往往效率会高很多。而另外一个方面就是算法中计算顺序的组织,而Halide可以改变的就是我们算法在某个硬件平台上的计算顺序:
其中Halide可以在硬件平台上为算法实现并行和良好的缓存一致性
Halide是一种编程语言,使得在现代机器上编写高性能图像和数组处理代码更加容易。
Halide并不是独立的编程语言,而是嵌入在C ++中。 这意味可以像编写c++代码一样编写halide代码,然后,可以将该表示形式编译为目标文件,或对其进行JIT编译并在同一过程中运行。
如果你想用普通的CPU做加速,又不想去优化算法,那么halide将是非常优秀的选择。唯一要做的,就是把算法用halide重写一遍即可。而且,halide重写的算法,就跟你正常理解的是完全一样的,不需要拐弯抹角。这在图像处理中非常有用。
英文教程可以参考:
https://halide-lang.org/tutorials/tutorial_introduction.html
API可以参考:
安装
Halide(一)环境配置 – Deep Studio (p-chao.com)
一、基本定义
#include "Halide.h" #include<iostream> using namespace Halide; using namespace std; int main(int argc, char** argv) { // 这个主函数定义了一个图像处理通道,用于获得灰度图像的对角梯度 // 'Func'对象代表一个处理管道,它规定了每个像素应该是怎么样的数据,可以理解为就是一幅处理后的图。例如opencv中的cv::mat Halide::Func gradientttt; // ‘Var’对象实际上就是类型,就像我们常说的int,float等。它本身是没有任何含义的。 Halide::Var x, y; // 我们用x和y来表示图像的x轴和y轴,按照我们处理图像的惯常思维,x就是图像列的索引,y就是图像行的索引。 // 就这些变量和其他函数而言,Func在其变量的任何整数坐标处定义为Expr。 在这里,我们将定义一个Expr,其值为x + y。 Var具有适当的运算符重载,因此“ x + y”之类的表达式成为“ Expr”对象。可以理解为表达式,或数学函数。 Halide::Expr e = x + y; // 现在我们为’Func’对象gradient进行定义。在x,y像素处,图像的像素值是Expr e的值。在等式的左边,就是我们定义的Func gradient对象和Vars变量x,y,在等式的右边就是用相同的Vars x,y定义的Expr e对象, gradientttt(x, y) = e; // 同样可以写为: // gradient(x, y) = x + y; // 这是更为一般的形式,但是为了完整起见,在此显示中间量Expr e。 // 该行代码定义了Func,但实际上并没有计算输出图像。在此阶段,内存中只有Funcs,Exprs和Vars,代表了我们成像管道的结构。我们正在元编程。这个C ++程序正在内存中构造一个Halide程序。而真正的像素数据计算紧随其后。 // 现在我们“实现” Func,JIT编译一些代码以实现我们定义的管道,然后运行它。我们还需要告诉Halide评估Func的域,该域确定上述x和y的范围以及输出图像的分辨率。Halide.h还提供了我们可以使用的基本模板图像类型。例如,我们生成一个800 x 600的图像,就需要定义一个域,也就是一个模板buffer<int32_t>。 Halide::Buffer<int32_t> output = gradientttt.realize({ 800, 600 });// 这个域内的全部实现? printf("%d\n", output.height()); printf("%d\n", output.width()); // Halide确实为您键入推断。Var对象代表32位整数,因此Expr对象'x + y'也代表32位整数,因此'gradient'定义了32位图像,因此当出现以下情况时,我们得到了32位有符号整数图像 我们称之为“实现”。halide类型和类型转换规则等效于C。 // 下面这个是验证程序,以期上面的结果跟下面展开的结果是一样的。 for (int j = 0; j < output.height(); j++) { for (int i = 0; i < output.width(); i++) { //printf("111Success!\n"); // 我们可以使用类似的语法来定义和使用函数,来访问Buffer对象的像素。 if (output(i, j) != i + j) { printf("Something went wrong!\n" "Pixel %d, %d was supposed to be %d, but instead it's %d\n", i, j, i + j, output(i, j)); return -1; } } } // 一切正常! 我们定义了一个Func,然后在其上调用“ realize”以生成并运行产生Buffer的机器代码。(暂且不说) printf("Success!\n"); getchar(); return 0; }
二、图像处理
本章演示如何传递输入图像并进行操作
#include <halide_image_io.h> #include <Halide.h> #include<iostream> #include<fstream> using namespace Halide::Tools; int main(int argc, char** argv) { assert(1 == 1); // 下面这个程序是对图像做亮度增强 // 首先导入图像s Halide::Buffer<uint8_t> input = load_image("C:\\Users\\fuhouyu\\Desktop\\8da1b2f36d67fdcd593c456a3b2985c4.png"); // 就是下面这个图 //参考下面的图21 // 下面定义一个代表这个图像处理管道的 Halide::Func brighter; // ”Func” 有三个参数,代表了图像的位置和颜色通道,halide把颜色通道作为图像的额外维度。 Halide::Var x, y, c; // 通常,我们可能会将整个函数定义写在一行上。 在这里,我们将其分解,以便我们可以解释我们在每个步骤中所做的事情。 // 对于输入图像的每个像素 Halide::Expr value = input(x, y, c); // 将其强制转换为浮点值 value = Halide::cast<float>(value); // 乘以1.5使其变亮。halide将实数表示为浮点数,而不是双精度数,因此我们在常数的末尾加上“ f”。 value = value * 2.0f; // 将其限制为小于255,因此将其强制转换回8位unsigned int时不会溢出。 value = Halide::min(value, 255.0f); // 将其强制转换为8位无符号整数。 value = Halide::cast<uint8_t>(value); // 定义函数 brighter(x, y, c) = value; // 将上面所有的等效成一行 // brighter(x, y, c) = Halide::cast<uint8_t>(min(input(x, y, c) * 1.5f, 255)); // 在这个版本中 // - 跳过了强制转换为浮点数,因为乘以1.5f会自动执行。 // - 还使用了整数常量作为对min的调用中的第二个参数,因为它被强制转换为float以便与第一个参数兼容。 // - 在调用min函数时,省略了halide::,因为使用了koenig lookup。(也叫做argument dependent lookup(ADL)。 // 请记住,到目前为止,我们所做的只是在内存中构建Halide程序的表示形式。 我们实际上尚未处理任何像素。我们甚至还没有编译该Halide程序。 // 因此,现在我们将实现Func。输出图像的尺寸应与输入图像的尺寸匹配。如果我们只是想加亮输入图像的一部分,我们可以要求使用较小的尺寸。如果我们请求更大的尺寸,那么Halide将在运行时抛出错误,告诉我们正试图读取输入图像上的边界。 Halide::Buffer<uint8_t> output = brighter.realize({ input.width(), input.height(), input.channels() }); // 保存输出以进行检查。它看起来像只鹦鹉。 save_image(output, "C:\\Users\\fuhouyu\\Desktop\\test1.png"); // 有关输出的小版本,请参见下文。 //参考下面的图22 printf("Success!\n"); return 0; }
第三章 检查生成的代码
本节演示了如何检查Halide编译器正在生成的内容。
#include "Halide.h" #include <stdio.h> // 这次我们将导入整个 Halide 命名空间 using namespace Halide; int main(int argc, char** argv) { // 我们将从第1课定义的简单的单阶段成像管道开始。 // 本课将涉及调试,但是不幸的是,在C ++中,对象不知道自己的名称,这使我们很难理解生成的代码。要解决此问题,您可以将字符串传递给Func和Var构造函数以为其命名,从而进行调试。也就是说,给对象一个名字,在调试的时候,只需要看这个名字,就知道这个变量是,值是多少。 Func gradient("gradient"); Var x("x"), y("y"); gradient(x, y) = x + y; // 实现该功能以产生输出图像。在本课程中,我们将使其保持很小的尺寸。 Buffer<int> output = gradient.realize({ 8, 8 }); // 那一行编译并运行了管道。 尝试在环境变量HL_DEBUG_CODEGEN设置为1的情况下运行本课程。它将打印出编译的各个阶段以及最终管道的伪代码表示。 // 如果将HL_DEBUG_CODEGEN设置为更高的数字,则可以看到有关Halide如何编译管道的越来越多的详细信息。设置 HL_DEBUG_CODEGEN = 2将显示编译的每个阶段的Halide代码,以及最后生成的llvm位代码。 // Halide还将输出此输出的HTML版本,该版本支持语法突出显示和代码折叠,因此对于大型管道而言,阅读起来会更好。运行本教程后,用浏览器打开gradient.html。 //gradient.compile_to_lowered_stmt("gradient.html", {}, HTML); // 通常,您可以找出Halide使用此伪代码生成的代码。 在下一课中,我们将看到如何在运行时监听Halide。 printf("Success!\n"); getchar(); return 0; }
第四章 使用tracing, print, 和print_when进行调试
本课演示如何在运行时遵循Halide的操作。
// 并告诉Halide我们希望收到所有评估的通知。 gradient.trace_stores();
// 现在,我们告诉Halide在y坐标上使用并行for循环。 在Linux上,我们使用线程池和任务队列来运行它。 在OS X上,我们调用大型中央调度,这对我们执行相同的操作。 parallel_gradient.parallel(y);
// 如果我们只想检查其中一个术语,则可以像这样包装“print”: Func g; g(x, y) = sin(x) + print(cos(y));
// 条件打印
// print和trace_stores均可产生大量输出。如果您正在寻找一个罕见的事件,或者只是想看看单个像素发生了什么,那么很难挖掘出如此大量的输出。可以使用函数print_when有条件地打印Expr。print_when的第一个参数是布尔Expr。 如果Expr的计算结果为true,则返回第二个参数并打印所有参数。如果Expr的计算结果为false,则仅返回第二个参数,并且不打印。
e = print_when(x == 37 && y == 42, e, "<- this is cos(y) at x, y == (37, 42)");
第五章 Vectorize, parallelize, unroll and tile 你的代码
// 对var执行的最强大的原始调度操作是将其拆分为内部和外部子变量:
Var x_outer, x_inner;
gradient.split(x, x_outer, x_inner, 2);
// 这将x上的循环分为两个嵌套循环:x_outer上的外部循环和x_inner上的内部循环。 拆分的最后一个参数是“split factor(分离因子)”。 内部循环从零到分离因子。 外循环从零开始到所需的范围x(在这种情况下为4)除以分离因子。 在循环中,旧变量定义为外部*因子+内部。 如果旧循环以非零值开始,则该值也将添加到循环中。
// 将两个变量融合为一个.
// 分离的相反是“融合”。 融合两个变量会将两个循环合并到扩展范围乘积上的单个for循环中。融合的重要性不如拆分,但它也很有用(我们将在本课程的后面看到)。像拆分一样,融合本身不会改变评估的顺序。
Var fused;
gradient.fuse(x, y, fused);
// 在图块中评估。 { Func gradient("gradient_tiled"); gradient(x, y) = x + y; gradient.trace_stores(); // 现在我们既可以拆分又可以重新排序,我们可以进行切片评估。让我们将x和y分别除以4,然后对var重新排序以表示块遍历。块遍历将域划分为小的矩形块,并且最外层迭代在块上,并且在其中进行遍历每个块内的点。 如果相邻像素使用重叠的输入数据(例如模糊),则可能对性能有好处。 我们可以这样表示块遍历: Var x_outer, x_inner, y_outer, y_inner; gradient.split(x, x_outer, x_inner, 4); gradient.split(y, y_outer, y_inner, 4); gradient.reorder(x_inner, y_inner, x_outer, y_outer); // 这种模式很常见,因此有一个简写: // gradient.tile(x, y, x_outer, y_outer, x_inner, y_inner, 4, 4); printf("Evaluating gradient in 4x4 tiles\n"); Buffer<int> output = gradient.realize({ 8, 8 }); }
// 用向量评估. { Func gradient("gradient_in_vectors"); gradient(x, y) = x + y; gradient.trace_stores(); // 拆分的好处在于,它确保内部变量从零到分割因子。在大多数情况下,分割因子将是编译时常量,因此我们可以使用单个向量来替换内部变量上的循环。这次我们将除以四,因为在X86上我们可以使用SSE来计算4宽向量。 Var x_outer, x_inner; gradient.split(x, x_outer, x_inner, 4); gradient.vectorize(x_inner); // 拆分然后向量化内部变量非常普遍,以至于它有一个简写形式。 我们还可以说: // gradient.vectorize(x, 4); // 等价于: // gradient.split(x, x, x_inner, 4); // gradient.vectorize(x_inner); // 请注意,在这种情况下,我们将名称“x”作为新的外部变量重用。以后引用x的调度调用将引用这个名为x的新外部变量。 // 这次,我们将对一个8x4的框进行评估,以便每条扫描线有多个工作向量. printf("Evaluating gradient with x_inner vectorized \n"); Buffer<int> output = gradient.realize({ 8, 4 }); }
(15条消息) 利用SSE计算向量点乘simd_dot_非文艺小燕儿_Vivien的博客-CSDN博客_sse 向量乘法
对于SSE,其实就是处理器中专门开辟了多个128位的寄存器。对于单精度浮点数,占用32bit,那么1个128bit的SSE寄存器,就可以存放4个单精度浮点数。对于单精度浮点数的运算指令,其实就相当于开了4个洞。比如,两个128位的SSE寄存器中存放的数据进行乘法运算,那么一次性就能得到4组运算结果。CPU的SIMD的处理能力是单核内的并行。向量处理在商业市场中仍然以DSP的形式存在,同时也被SSE和AVX指令集支持。将多次for循环计算变成一次计算完全仰仗于CPU的SIMD指令集,SIMD指令可以在一条cpu指令上处理2、4、8或者更多份的数据。在Intel处理器上,这个称之为SSE以及后来的AVX,在Arm处理上,这个称之为NEON。
cpu的处理粒度?一个寄存器里存放多个数据怎么保证处理时不互相干扰?
// 展开循环.把最内侧的for循环改成硬编码 { Func gradient("gradient_unroll"); gradient(x, y) = x + y; gradient.trace_stores(); // 如果多个像素共享重叠的数据,则展开计算是有意义的,以便共享值仅计算或加载一次。我们这样做与表示向量化的方式类似。我们拆分一个维度,然后完全展开内部变量的循环。展开不会改变评估的顺序. Var x_outer, x_inner; gradient.split(x, x_outer, x_inner, 2); gradient.unroll(x_inner); // 简写为: // gradient.unroll(x, 2); printf("Evaluating gradient unrolled by a factor of two\n"); Buffer<int> result = gradient.realize(4, 4);
}
// 融合,平铺和并行化. { // 在上一课中,我们看到了可以对变量进行并行化的过程。 在这里,我们将其与融合和平铺结合起来以表达有用的模式-并行处理图块。 // 这是融合的光芒。 当您想跨多个维度并行化而不引入嵌套并行性时,融合会有所帮助。嵌套并行性(并行for循环中的parallel for循环)受Halide支持,但与将并行变量融合到单个并行for循环中相比,性能通常较差。 Func gradient("gradient_fused_tiles"); gradient(x, y) = x + y; gradient.trace_stores(); // 首先,我们将平铺,然后将平铺索引融合在一起,并在组合中进行并行化。 Var x_outer, y_outer, x_inner, y_inner, tile_index; gradient.tile(x, y, x_outer, y_outer, x_inner, y_inner, 4, 4); gradient.fuse(x_outer, y_outer, tile_index); gradient.parallel(tile_index); // 调度调用全部返回对Func的引用,因此您也可以将它们链接到一个语句中,以使事情更加清晰: // gradient // .tile(x, y, x_outer, y_outer, x_inner, y_inner, 2, 2) // .fuse(x_outer, y_outer, tile_index) // .parallel(tile_index); printf("Evaluating gradient tiles in parallel\n"); Buffer<int> output = gradient.realize(8, 8); }
// 融合到一起. { // 你准备好了吗? 现在,我们将使用上面的所有功能。 Func gradient_fast("gradient_fast"); gradient_fast(x, y) = x + y; // 我们将并行处理64x64片。 Var x_outer, y_outer, x_inner, y_inner, tile_index; gradient_fast .tile(x, y, x_outer, y_outer, x_inner, y_inner, 64, 64) .fuse(x_outer, y_outer, tile_index) .parallel(tile_index); // 当我们跨过每个图块时,我们将一次计算两条扫描线。 我们还将在x中向量化。 最简单的表达方式是在每个图块中再次递归地将其划分为4x2子图块,然后将x上的图块矢量化并在y上展开它们: Var x_inner_outer, y_inner_outer, x_vectors, y_pairs; gradient_fast .tile(x_inner, y_inner, x_inner_outer, y_inner_outer, x_vectors, y_pairs, 4, 2) .vectorize(x_vectors) .unroll(y_pairs); // 请注意,我们没有进行任何显式的拆分或重新排序。 这些是最重要的原始操作,但是大多数情况下,它们埋藏在平铺,矢量化或展开调用之下。 // 现在让我们在不是图块大小倍数的范围内进行评估。 // 如果愿意,您可以打开跟踪,但是它将产生很多printfs。 相反,我们将同时使用C语言和Halide计算答案,并查看答案是否匹配。 Buffer<int> result = gradient_fast.realize(350, 250); }
请注意,在Halide版本中,算法与优化分开在算法顶部指定一次,并且总共没有多少行代码。将此与C版本进行比较。还有更多的代码(甚至没有并行化或向量化)。更令人讨厌的是,算法的语句(结果是x加y)被埋在混乱中的多个地方。 此C代码难以编写,难以读取,难以调试以及难以进一步优化。 这就是halide存在的原因。
在任意域上实现函数
// 现在让我们计算一个从其他地方开始的5x 7矩形上的梯度——位置(100,50)。所以x和y从(100,50)到(104,56)。 // 我们首先创建一个表示矩形的图像: Buffer<int> shifted(5, 7); // 在构造器中我们告诉它大小. shifted.set_min(100, 50); // 然后我们在左上角告诉它. printf("Evaluating gradient from (100, 50) to (104, 56)\n"); // 注意,这不需要编译任何新代码,因为当我们第一次实现它时,我们生成了能够计算任意矩形上的梯度的代码。 gradient.realize(shifted);
多级管道(类似于函数调用函数)
#include "Halide.h" #include <stdio.h> using namespace Halide; // 加载PNG的支持代码. #include "halide_image_io.h" using namespace Halide::Tools; int main(int argc, char **argv) { // 首先,我们将在下面声明一些要使用的变量. Var x("x"), y("y"), c("c"); // 现在我们将表示一个多级管道,它先水平模糊图像,然后垂直模糊图像. { // 获取8位彩色输入 Buffer<uint8_t> input = load_image("images/rgb.png"); // 把它升级到16位,这样我们就可以计算而不会溢出. Func input_16("input_16"); input_16(x, y, c) = cast<uint16_t>(input(x, y, c)); // 水平模糊: Func blur_x("blur_x"); blur_x(x, y, c) = (input_16(x-1, y, c) + 2 * input_16(x, y, c) + input_16(x+1, y, c)) / 4; // 垂直模糊: Func blur_y("blur_y"); blur_y(x, y, c) = (blur_x(x, y-1, c) + 2 * blur_x(x, y, c) + blur_x(x, y+1, c)) / 4; // 转换回8位. Func output("output"); output(x, y, c) = cast<uint8_t>(blur_y(x, y, c)); // 此管道中的每个Func都使用熟悉的函数调用语法调用前一个Func(我们在Func对象上重载了operator())。Func可以调用已给出定义的任何其他Func。此限制可防止管道中包含循环。halide管道总是Funcs的前向图. // 现在让我们实现它... // Buffer<uint8_t> result = output.realize(input.width(), input.height(), 3); // 只是上面这一行行不通。取消注释以查看发生了什么。 // 在与输入图像相同的域上实现此管道需要读取超出输入边界的像素,因为blur_xstage水平向外延伸,blur_ystage垂直向外延伸。Halide通过在管道顶部注入一段代码来检测这一点,该代码计算将在其上读取输入的区域。当它开始运行管道时,它首先运行此代码,确定将读取超出界限的输入,并拒绝继续。在内部循环中没有实际的边界检查;这会很慢。 //也就是说要注意图像边界问题,防止越界 // 那我们该怎么办?有几个选择。如果我们意识到在一个域上向内移动了一个像素,我们就不会要求halide程序读取越界。我们在上一课中看到了如何做到这一点: Buffer<uint8_t> result(input.width()-2, input.height()-2, 3); result.set_min(1, 1); output.realize(result); // 保存结果。它应该看起来像一只略带模糊的鹦鹉,并且应该比输入图像窄两个像素,短两个像素(这是因为边界的问题)。 save_image(result, "blurry_parrot_1.png"); //这通常是处理边界的最快方法:不要编写读取越界的代码:)下一个示例是更一般的解决方案。 } // 相同的管道,在输入端有一个边界条件. { // 获取8位彩色输入 Buffer<uint8_t> input = load_image("images/rgb.png"); // 这次,我们将把输入包装在一个Func中,以防止读取超出界限: Func clamped("clamped"); // 定义一个表达式,将x钳制在[0,input.width()-1]范围内。 Expr clamped_x = clamp(x, 0, input.width()-1); // clamp(x, a, b) 等价于 max(min(x, b), a). // 类似的对y进行限制. Expr clamped_y = clamp(y, 0, input.height()-1); // 在限制的范围内读取图像。这意味着无论我们如何计算Func‘clapped’,我们永远不会读取输入的越界值。这是一个钳制到边的边界条件,是用halide表示的最简单的边界条件。 clamped(x, y, c) = input(clamped_x, clamped_y, c); // 使用BoundaryConditions命名空间中的helper函数可以更简洁地定义“clamped”,如下所示: // clamped = BoundaryConditions::repeat_edge(input); // 这些对于其他边界条件的使用很重要,因为它们以halide能够最好地理解和优化的方式表示。如果使用正确,它们和没有边界条件一样好用。 // 将它升级到16位,这样我们就可以在不溢出的情况下进行计算。这次我们将引用我们的新函数“clamped”,而不是直接引用输入图像。 Func input_16("input_16"); input_16(x, y, c) = cast<uint16_t>(clamped(x, y, c)); // 其余的管道都是一样的... // 水平模糊: Func blur_x("blur_x"); blur_x(x, y, c) = (input_16(x-1, y, c) + 2 * input_16(x, y, c) + input_16(x+1, y, c)) / 4; // 垂直模糊: Func blur_y("blur_y"); blur_y(x, y, c) = (blur_x(x, y-1, c) + 2 * blur_x(x, y, c) + blur_x(x, y+1, c)) / 4; // 转换为 8-bit. Func output("output"); output(x, y, c) = cast<uint8_t>(blur_y(x, y, c)); // 这一次可以安全地计算某个域上的输出作为输入,因为我们有一个边界条件。 Buffer<uint8_t> result = output.realize(input.width(), input.height(), 3); // 保存结果。它看起来像一只略带模糊的鹦鹉,但这次它的大小与输入的大小相同。 save_image(result, "blurry_parrot_2.png"); } printf("Success!\n"); return 0; }
多重传递函数、更新定义和约化
更新定义
// 对于f的每个实现,每个步骤都在下一个步骤开始之前完整地运行。一个简单的例子,让我们跟踪负载和存储: Func g("g"); g(x, y) = x + y; // 纯定义 g(2, 1) = 42; // 第一次更新定义 g(x, 0) = g(x, 1); // 第二次更新定义 g.trace_loads(); g.trace_stores(); g.realize(4, 4);
// 将更新过程放入循环中. { // 从这个纯粹的定义开始: Func f; f(x, y) = (x + y)/100.0f; // 假设我们想要一个更新,使前五十行成正方形。我们可以添加50个更新定义: // f(x, 0) = f(x, 0) * f(x, 0); // f(x, 1) = f(x, 1) * f(x, 1); // f(x, 2) = f(x, 2) * f(x, 2); // ... // f(x, 49) = f(x, 49) * f(x, 49); // 或者在C++中等效使用编译时间循环: // for (int i = 0; i < 50; i++) { // f(x, i) = f(x, i) * f(x, i); // } // 但将循环放入生成的代码中更易于管理,也更灵活。我们通过定义一个“还原域”并在更新定义中使用它来实现这一点: RDom r(0, 50); f(x, r) = f(x, r) * f(x, r); Buffer<float> halide_result = f.realize(100, 100); }