本文是halide编程指南的连载,已同步至公众号
第 15章: 生成器 第一节
// 本课程演示如何将halide管道封装到可重用组件(称为生成器)中.
// linux这样编译运行:
// g++ lesson_15*.cpp <path/to/tools/halide_image_io.h>/GenGen.cpp -g -std=c++11 -fno-rtti -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -lpthread -ldl -o lesson_15_generate
// bash lesson_15_generators_usage.sh
// os x:
// g++ lesson_15*.cpp <path/to/tools/halide_image_io.h>/GenGen.cpp -g -std=c++11 -fno-rtti -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -o lesson_15_generate
// bash lesson_15_generators_usage.sh
// 在源码树上可以这样运行:
// make tutorial_lesson_15_generators
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
// 生成器是提前编译halide管道的一种更结构化的方法。我们定义了一个继承自Halide::Generator的类,而不是像第10课那样使用特殊命令行接口编写int main().
class MyFirstGenerator : public Halide::Generator<MyFirstGenerator>
{
public:
// 我们将halide管道的输入声明为公共成员变量。它们将以声明它们的相同顺序出现在生成函数的签名中.
Input<uint8_t> offset{"offset"};
Input<Buffer<uint8_t>> input{"input", 2};
// 我们还将输出声明为公共成员变量.
Output<Buffer<uint8_t>> brighter{"brighter", 2};
// 通常您也在这个范围内声明变量,以便可以在以后添加的任何辅助方法中使用它们.
Var x, y;
// 然后我们定义一个方法来构造并返回halide管道:
void generate() {
// 在第10章,我们称之为Func::compile_to_file. 在生成器中,我们只需要定义表示管道输出的输出.
brighter(x, y) = input(x, y) + offset;
// 安排.
brighter.vectorize(x, 16).parallel(y);
}};
// 我们用tools/GenGen.cpp编译这个文件. 这个文件定义了一个 "int main(...)" 它提供了使用生成器类的命令行接口。我们得把生成器的代码告诉他. 这样做:
HALIDE_REGISTER_GENERATOR(MyFirstGenerator, my_first_generator)
// 如果您愿意,可以在一个文件中放置多个生成器。如果他们共享一些公共代码,这可能是个好主意。让我们定义另一个更复杂的生成器:
class MySecondGenerator : public Halide::Generator<MySecondGenerator>
{
public:
// 这个生成器也需要一些编译时参数。这使您可以编译halide物管道的多个变体。我们将定义一个,它告诉我们是否在我们的时间表中并行化:
GeneratorParam<bool> parallel{"parallel", /* default value */ true};
// ... 另一个表示要使用的恒定比例因子:
GeneratorParam<float> scale{"scale",
1.0f /* default value */,
0.0f /* minimum value */,
100.0f /* maximum value */};
// 可以定义所有基本标量类型的生成器参数。对于数字类型,您可以选择提供最小值和最大值,就像我们在上面的scale中所做的那样.
// 还可以为枚举定义生成器参数。要实现这一点,必须提供从字符串到枚举值的映射.
enum class Rotation { None,
Clockwise,
CounterClockwise };
GeneratorParam<Rotation> rotation{"rotation",
/* default value */
Rotation::None,
/* map from names to values */
{{"none", Rotation::None},
{"cw", Rotation::Clockwise},
{"ccw", Rotation::CounterClockwise}}};
// 我们将使用与以前相同的输入:
Input<uint8_t> offset{"offset"};
Input<Buffer<uint8_t>> input{"input", 2};
// 以及类似的输出。注意,我们没有为缓冲区指定类型:在编译时,我们必须通过输出类型“GeneratorParam(为该输出隐式定义).
Output<Buffer<>> output{"output", 2};
// 我们会像以前一样在这里申明我们的VAR.
Var x, y;
void generate() {
// 定义函数。我们将使用编译时比例因子和运行时偏移参数.
Func brighter;
brighter(x, y) = scale * (input(x, y) + offset);
//我们可能会做一些旋转,这取决于枚举。要获取GeneratorParam的值,请将其强制转换为相应的类型。这种类型转换大多数时候都是隐式发生的(例如,上面的scale).
Func rotated;
switch ((Rotation)rotation) {
case Rotation::None:
rotated(x, y) = brighter(x, y);
break;
case Rotation::Clockwise:
rotated(x, y) = brighter(y, 100 - x);
break;
case Rotation::CounterClockwise:
rotated(x, y) = brighter(100 - y, x);
break;
}
// 然后,我们将强制转换为所需的输出类型.
output(x, y) = cast(output.type(), rotated(x, y));
// 管道的结构取决于GeneratorParam。时间表也一样.
// 让我们从矢量化输出开始。我们不知道类型,所以很难选择一个好的因素。生成器提供了一个名为“natural_vector_size”的助手,该助手将根据您要编译的类型和目标为您选择一个合理的因子.
output.vectorize(x, natural_vector_size(output.type()));
// 现在我们可能将其并行化:
if (parallel) {
output.parallel(y);
}
// 如果有一个旋转,我们会安排它在输出的每个扫描线上发生,并根据它的类型对它进行矢量化.
if (rotation != Rotation::None) {
rotated
.compute_at(output, y)
.vectorize(x, natural_vector_size(rotated.output_types()[0]));
}
}};
// 注册我们的第二个生成器:
HALIDE_REGISTER_GENERATOR(MySecondGenerator, my_second_generator)
// 编译完此文件后,请参阅如何使用它// lesson_15_generators_build.sh
第 15章: 生成器 第二节
# 这个shell脚本演示如何从命令行使用包含生成器的二进制文件。通常,您会从您选择的构建系统中调用这些二进制文件,而不是像我们这里所做的那样手动运行它们.
# 此脚本假定您位于tutorials目录中,并且生成器已为当前系统编译,名为“lesson_15_generate”.
# 运行此脚本:# bash lesson_15_generators_usage.sh
# 首先,我们定义一个helper函数来检查文件是否存在
check_file_exists(){
FILE=$1
if [ ! -f $FILE ]; then
echo $FILE not found
exit -1
fi}
# 以及另一个助手函数,用于检查对象文件中是否存在符号
check_symbol(){
FILE=$1
SYM=$2
if !(nm $FILE | grep $SYM > /dev/null); then
echo "$SYM not found in $FILE"
exit -1
fi}
# 出错时退出
#set -e
# 设置 LD_LIBRARY_PATH 以便能找到libHalide.so
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:../libexport DYLD_LIBRARY_PATH=${DYLD_LIBRARY_PATH}:../lib
########################## 生成器基本用途 ##########################
# 首先,让我们为主机系统编译第一个生成器:
./lesson_15_generate -g my_first_generator -o . target=host
# 应该在当前目录中创建一对文件: "my_first_generator.a", 和"my_first_generator.h",它定义了一个表示已编译管道的函数“my_first_generator”.
check_file_exists my_first_generator.a
check_file_exists my_first_generator.h
check_symbol my_first_generator.a my_first_generator
###################### 交叉编译 ######################
# 我们还可以使用生成器为其他一些目标编译对象文件。让我们为第一个生成器交叉编译一个windows 32位对象文件和头文件:
./lesson_15_generate \
-g my_first_generator \
-f my_first_generator_win32 \
-o . \
target=x86-32-windows
# 这将在当前目录中生成一个名为“my_first_generator_win32.lib”的文件,以及一个匹配的头文件。定义的函数名为“my_first_generator_win32” .
check_file_exists my_first_generator_win32.lib
check_file_exists my_first_generator_win32.h
################################# 生成管道变体 #################################
# 生成器二进制文件的完整命令行参数集如下:
# -g generator_name : 选择要运行的生成器。如果二进制文件中只有一个生成器,则可以忽略此项.
# -o directory : 指定在哪个目录中创建输出。通常是build目录.
# -f name : 指定生成的函数的名称。如果忽略此项,则默认为生成器名称.
# -n file_base_name : 指定生成的文件的basename。如果省略此项,则默认为生成的函数的名称.
# -e static_library,object,c_header,assembly,bitcode,stmt,stmt_html: 指定要创建的输出的逗号分隔值的列表。默认值为“static_library,c_header,registration”,“assembly“相当于生成的对象文件的程序集。”bitcode“为管道生成llvm位码。”stmt“为管道生成人类可读的伪代码(类似于设置HL_DEBUG_CODEGEN)。”stmt_html“生成伪代码的html版本,它比原始的.stmt文件更易于读取
# -r file_base_name : 指定生成器应仅为运行时创建独立文件。用于从单个生成器生成多个管道时,在一个可执行文件中链接在一起。参见下面的示例.
# target=... : 要编译的目标.
# my_generator_param=value : 生成器参数的值.
# 现在让我们为第一个生成器生成一些人类可读的伪代码:
./lesson_15_generate -g my_first_generator -e stmt -o . target=host
check_file_exists my_first_generator.stmt
# 第二个生成器具有生成器参数,可以在目标后面的命令行上指定。让我们编译几个不同的变体:
./lesson_15_generate -g my_second_generator -f my_second_generator_1 -o . \
target=host parallel=false scale=3.0 rotation=ccw output.type=uint16
./lesson_15_generate -g my_second_generator -f my_second_generator_2 -o . \
target=host scale=9.0 rotation=ccw output.type=float32
./lesson_15_generate -g my_second_generator -f my_second_generator_3 -o . \
target=host parallel=false output.type=float64
check_file_exists my_second_generator_1.a
check_file_exists my_second_generator_1.h
check_symbol my_second_generator_1.a my_second_generator_1
check_file_exists my_second_generator_2.a
check_file_exists my_second_generator_2.h
check_symbol my_second_generator_2.a my_second_generator_2
check_file_exists my_second_generator_3.a
check_file_exists my_second_generator_3.h
check_symbol my_second_generator_3.a my_second_generator_3
# 这些生成的对象文件和头的用法与第10课完全相同.
####################### Halide runtime #######################
# 每个生成的halide对象文件都包含一个简单的运行时,它定义了如何运行并行for循环、如何启动cuda程序等内容。您可以在生成的对象文件中看到这个运行时.
echo "The halide runtime:"
nm my_second_generator_1.a | grep "[SWT] _\?halide_"
# 让我们定义一些函数来检查运行时是否存在于文件中.
check_runtime(){
if !(nm $1 | grep "[TSW] _\?halide_" > /dev/null); then
echo "Halide runtime not found in $1"
exit -1
fi}
check_no_runtime(){
if nm $1 | grep "[TSW] _\?halide_" > /dev/null; then
echo "Halide runtime found in $1"
exit -1
fi}
# 这些运行时函数的声明和文档在HalideRuntime.h中
# 如果您正在编译和链接多个halide管道,那么运行时的多个副本应该合并为一个副本(通过弱链接)。如果您正在为多个不同的目标(例如avx和非avx)编译和链接,那么运行时可能不同,并且您无法控制链接器选择的运行时副本.
# 您可以通过使用no_runtime目标标志编译管道来显式控制此行为。让我们为不同的x86变体生成并链接第一个管道的几个不同版本:
# (请注意,我们将要求生成器只提供对象文件(“-e o”),而不是静态库,这样我们就可以轻松地将它们链接到单个静态库中.)
./lesson_15_generate \
-g my_first_generator \
-f my_first_generator_basic \
-e object,c_header\
-o . \
target=host-x86-64-no_runtime
./lesson_15_generate \
-g my_first_generator \
-f my_first_generator_sse41 \
-e object,c_header\
-o . \
target=host-x86-64-sse41-no_runtime
./lesson_15_generate \
-g my_first_generator \
-f my_first_generator_avx \
-e object,c_header\
-o . \
target=host-x86-64-avx-no_runtime
# 这些文件不包含运行时
check_no_runtime my_first_generator_basic.o
check_symbol my_first_generator_basic.o my_first_generator_basic
check_no_runtime my_first_generator_sse41.o
check_symbol my_first_generator_sse41.o my_first_generator_sse41
check_no_runtime my_first_generator_avx.o
check_symbol my_first_generator_avx.o my_first_generator_avx
# 然后我们可以使用生成器来发出运行时:
./lesson_15_generate \
-r halide_runtime_x86 \
-e object,c_header\
-o . \
target=host-x86-64
check_runtime halide_runtime_x86.o
# 通过将独立运行时与三个生成的对象文件相链接,我们得到了适用于不同级别x86的三个版本的管道,并结合了一个可以在几乎所有x86处理器上运行的运行时.
ar q my_first_generator_multi.a \
my_first_generator_basic.o \
my_first_generator_sse41.o \
my_first_generator_avx.o \
halide_runtime_x86.o
check_runtime my_first_generator_multi.a
check_symbol my_first_generator_multi.a my_first_generator_basic
check_symbol my_first_generator_multi.a my_first_generator_sse41
check_symbol my_first_generator_multi.a my_first_generator_avx
echo "Success!"
第16章: RGB图像和内存布局 第1部分
// 本课程演示如何以交错或平面格式赋值halide RGB图像,以及如何编写针对每种情况优化的代码.
// 在 linux 或os x, 编译运行:
// g++ lesson_16_rgb_generate.cpp <path/to/tools/halide_image_io.h>/GenGen.cpp -g -std=c++11 -fno-rtti -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -lpthread -ldl -o lesson_16_generate
// export LD_LIBRARY_PATH=<path/to/libHalide.so>
# 对于 linux
// export DYLD_LIBRARY_PATH=<path/to/libHalide.dylib>
# 对于 OS X
// ./lesson_16_generate -g brighten -o . -f brighten_planar target=host layout=planar
// ./lesson_16_generate -g brighten -o . -f brighten_interleaved target=host layout=interleaved
// ./lesson_16_generate -g brighten -o . -f brighten_either target=host layout=either
// ./lesson_16_generate -g brighten -o . -f brighten_specialized target=host layout=specialized
// g++ lesson_16_rgb_run.cpp brighten_*.o -ldl -lpthread -o lesson_16_run
// ./lesson_16_run
// 有原码树,也可以这样运行:
// make tutorial_lesson_16_rgb_run.
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
// 我们将定义一个生成器器,使RGB图像变亮.
class Brighten : public Halide::Generator<Brighten>
{
public:
// 我们声明一个三维输入图像。前两个维度是x和y,第三个维度是颜色通道.
Input<Buffer<uint8_t>> input{"input", 3};
// 我们将以几种方式编译这个生成器,以便为输入和输出接受几种不同的内存布局。这是GeneratorParam的一个很好的用法(参见第15课).
enum class Layout { Planar,
Interleaved,
Either,
Specialized };
GeneratorParam<Layout> layout{"layout",
// default value
Layout::Planar,
// map from names to values
{{"planar", Layout::Planar},
{"interleaved", Layout::Interleaved},
{"either", Layout::Either},
{"specialized", Layout::Specialized}}};
// 我们还声明了一个标量输入来控制亮度的大小.
Input<uint8_t> offset{"offset"};
// 申明输出
Output<Buffer<uint8_t>> brighter{"brighter", 3};
// 申明 Vars
Var x, y, c;
void generate() {
// 定义 Func.
brighter(x, y, c) = input(x, y, c) + offset;
// 安排.
brighter.vectorize(x, 16);
// 根据“layout”generatorparam,我们将编译这个管道以几种不同的方式处理内存布局.
if (layout == Layout::Planar) {
// 这个管道只适用于每个扫描线都是密集的单色通道的图像。根据第10课中描述的步幅,Halide假设并断言x中的步幅是1.
// 此约束允许平面图像,其中红色、绿色和蓝色通道在内存中的布局如下:
// RRRRRRRR
// RRRRRRRR
// RRRRRRRR
// RRRRRRRR
// GGGGGGGG
// GGGGGGGG
// GGGGGGGG
// GGGGGGGG
// BBBBBBBB
// BBBBBBBB
// BBBBBBBB
// BBBBBBBB
// 它也适用于不太常用的逐行布局,在这种布局中,红色、绿色和蓝色的扫描线交替出现.
// RRRRRRRR
// GGGGGGGG
// BBBBBBBB
// RRRRRRRR
// GGGGGGGG
// BBBBBBBB
// RRRRRRRR
// GGGGGGGG
// BBBBBBBB
// RRRRRRRR
// GGGGGGGG
// BBBBBBBB
} else if (layout == Layout::Interleaved) {
// 另一种常见的格式是“交错”,即每个像素的红色、绿色和蓝色值在内存中相邻出现:
// RGBRGBRGBRGBRGBRGBRGBRGB
// RGBRGBRGBRGBRGBRGBRGBRGB
// RGBRGBRGBRGBRGBRGBRGBRGB
// RGBRGBRGBRGBRGBRGBRGBRGB
// 在这种情况下,x中的步幅是3,y中的步幅是图像宽度的3倍,c中的步幅是1。我们可以让Halide假设(并断言)输入和输出是这样的:
input.dim(0).set_stride(3); // x中的步幅为3
input.dim(2).set_stride(1); // c中的步幅为1
brighter.dim(0).set_stride(3);
brighter.dim(2).set_stride(1);
// 对于交错布局,可能需要使用不同的明细表。我们将告诉Halide另外假设并断言有三个颜色通道,然后利用这一事实使“c”最里面的循环展开.
input.dim(2).set_bounds(0, 3); // 维度2(c)从0开始,范围为3.
brighter.dim(2).set_bounds(0, 3);
// 将循环移到最里面的颜色通道上并展开它.
brighter.reorder(c, x, y).unroll(c);
// 注意,如果我们处理的是一个带有alpha通道(RGBA)的图像,那么x中的步幅和通道维度的边界都将是4而不是3.
} else if (layout == Layout::Either) {
// 我们还可以删除所有约束并编译一个管道,该管道将与任何内存布局一起工作。它可能会很慢,因为所有向量加载都会成为聚集,而所有向量存储都会成为分散.
input.dim(0).set_stride(Expr()); // 使用默认的未定义表达式表示没有约束.
brighter.dim(0).set_stride(Expr());
} else if (layout == Layout::Specialized) {
// 我们可以通过让Halide在运行时检查内存布局,并根据找到的步长,切换到对应的分支,来接受任何性能良好的内存布局。首先,我们放松默认的约束dim(0).stride()==1:
input.dim(0).set_stride(Expr()); // 使用未定义的表达式表示没有约束.
brighter.dim(0).set_stride(Expr());
// 我们构造布尔表达式,在运行时检测我们是平面的还是交错的。条件应该检查我们在每种情况下想要利用的所有事实.
Expr input_is_planar =
(input.dim(0).stride() == 1);
Expr input_is_interleaved =
(input.dim(0).stride() == 3 &&
input.dim(2).stride() == 1 &&
input.dim(2).extent() == 3);
Expr output_is_planar =
(brighter.dim(0).stride() == 1);
Expr output_is_interleaved =
(brighter.dim(0).stride() == 3 &&
brighter.dim(2).stride() == 1 &&
brighter.dim(2).extent() == 3);
// 然后,我们可以使用Func::specialize编写一个调度,该调度在运行时切换到基于布尔表达式的专用代码。该代码将利用已知表达式为真的事实.
brighter.specialize(input_is_planar && output_is_planar);
// 我们已经矢量化和并行化了,我们的两个专门化将继承这些调度指令。我们还可以添加仅适用于单个专门化的其他调度指令。我们将告诉Halide为交错布局生成专门的代码版本,并重新排序和展开专门的代码.
brighter.specialize(input_is_interleaved && output_is_interleaved)
.reorder(c, x, y)
.unroll(c);
// 如果输入是交错的,输出是平面的,我们也可以添加专门化,反之亦然,但是两个专门化就足以演示这个特性。后面的教程将探讨Func::specialize更具创造性的用法.
// 添加专门化可以大大提高它们所应用的情况的性能,但也会增加编译和发布的代码量。如果二进制大小是一个问题,并且输入和输出内存布局是已知的,那么您可能希望改用set_stride和set_extent
}
}};
// 与第15课一样,我们注册生成器,然后用工具编译这个文件 tools/GenGen.cpp.
HALIDE_REGISTER_GENERATOR(Brighten, brighten)
// 编译完此文件后,请参见lesson_16_rgb_run.cpp如何使用
第16章: RGB图像和内存布局 第2部分
// 阅读下面的之前,请先看 lesson_16_rgb_generate.cpp
// 这是实际使用我们编译的halide管道的代码。它不依赖于libHalide,因此我们将不包括Halide.h。相反,它取决于lesson_16_rgb_generator生成的头文件.
#include "brighten_either.h"
#include "brighten_interleaved.h"
#include "brighten_planar.h"
#include "brighten_specialized.h"
// 我们将使用Halide::Runtime::Buffer类将数据传入和传出管道.
#include "HalideBuffer.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "halide_benchmark.h"
void check_timing(double faster, double slower) {
if (faster > slower) {
fprintf(stderr, "Warning: performance was worse than expected. %f should be less than %f\n", faster, slower);
}}
int main(int argc, char **argv) {
// 让我们用交错和平面存储器来存储一些图像。默认情况下,Halide::Runtime::Buffer是平面的.
Halide::Runtime::Buffer<uint8_t> planar_input(1024, 768, 3);
Halide::Runtime::Buffer<uint8_t> planar_output(1024, 768, 3);
Halide::Runtime::Buffer<uint8_t> interleaved_input =
Halide::Runtime::Buffer<uint8_t>::make_interleaved(1024, 768, 3);
Halide::Runtime::Buffer<uint8_t> interleaved_output =
Halide::Runtime::Buffer<uint8_t>::make_interleaved(1024, 768, 3);
// 考虑到在生成器中设置的约束,让我们检查一下步长是否符合预期.
assert(planar_input.dim(0).stride() == 1);
assert(planar_output.dim(0).stride() == 1);
assert(interleaved_input.dim(0).stride() == 3);
assert(interleaved_output.dim(0).stride() == 3);
assert(interleaved_input.dim(2).stride() == 1);
assert(interleaved_output.dim(2).stride() == 1);
// 现在我们将调用我们编译的各种函数并检查每个函数的性能.
constexpr int samples = 1;
constexpr int iterations = 1000;
// 在平面图像上运行代码的平面版本,在交错图像上运行代码的交错版本。我们将使用Halide的benchmarking实用程序,它需要运行一个函数、要运行的批数(本例中为1)和每个批的迭代次数(本例中为1000)。它返回最佳平均迭代时间(以秒为单位)。(有关更多信息,请参见halide_benchmark.h。)
double planar_time = Halide::Tools::benchmark(samples, iterations, [&]() {
brighten_planar(planar_input, 1, planar_output);
});
printf("brighten_planar: %f msec\n", planar_time * 1000.f);
double interleaved_time = Halide::Tools::benchmark(samples, iterations, [&]() {
brighten_interleaved(interleaved_input, 1, interleaved_output);
});
printf("brighten_interleaved: %f msec\n", interleaved_time * 1000.f);
// 对于大多数成像操作,平面通常比交错更快.
check_timing(planar_time, interleaved_time);
// 接下来的两个注释掉的调用中的任何一个都会抛出错误,因为步长不是我们在生成器中承诺的那样.
// brighten_planar(interleaved_input, 1, interleaved_output);
// Error: Constraint violated: brighter.stride.0 (3) == 1 (1)
// brighten_interleaved(planar_input, 1, planar_output);
// Error: Constraint violated: brighter.stride.0 (1) == 3 (3)
// 运行灵活版本的代码并检查性能。它应该可以工作,但它会比上面的版本慢.
double either_planar_time = Halide::Tools::benchmark(samples, iterations, [&]() {
brighten_either(planar_input, 1, planar_output);
});
printf("brighten_either on planar images: %f msec\n", either_planar_time * 1000.f);
check_timing(planar_time, either_planar_time);
double either_interleaved_time = Halide::Tools::benchmark(samples, iterations, [&]() {
brighten_either(interleaved_input, 1, interleaved_output);
});
printf("brighten_either on interleaved images: %f msec\n", either_interleaved_time * 1000.f);
check_timing(interleaved_time, either_interleaved_time);
// 在每个布局上运行代码的专用版本。它应该通过内部分支到等效代码来匹配专门为上述每种情况编译的代码的性能.
double specialized_planar_time = Halide::Tools::benchmark(samples, iterations, [&]() {
brighten_specialized(planar_input, 1, planar_output);
});
printf("brighten_specialized on planar images: %f msec\n", specialized_planar_time * 1000.f);
// if语句的成本应该可以忽略不计,但是我们将允许50%的公差来考虑测量噪声.
check_timing(specialized_planar_time, 1.5 * planar_time);
double specialized_interleaved_time = Halide::Tools::benchmark(samples, iterations, [&]() {
brighten_specialized(interleaved_input, 1, interleaved_output);
});
printf("brighten_specialized on interleaved images: %f msec\n", specialized_interleaved_time * 1000.f);
check_timing(specialized_interleaved_time, 2.0 * interleaved_time);
return 0;
}