文章

Intel GPU 技术解析(一):图像 Tiling 的底层原理与性能实验

从 Linear 与 Tiled 图像布局的差异出发,拆解 Intel GPU 的地址生成、X/Y/Yf/Tile4/Tile64、format modifier、内存 swizzle 与 CCS,并用两个零依赖 C++ 实验观察性能影响。

Intel GPU 技术解析(一):图像 Tiling 的底层原理与性能实验

一张图像在 API 中通常只是宽、高、格式和若干像素,但到了 GPU 内存里,它未必按照“第一行、第二行、第三行”的顺序存放。Intel GPU 会把很多纹理、颜色缓冲和深度缓冲保存成 Tiled Surface:将二维图像划成小块,并重新排列 X、Y 坐标位与地址位的对应关系。

Tiling 不会减少图像本身的数据量。它把 GPU 常用的二维邻域集中到更少的缓存行、内存页和内存事务中,从而降低数据搬运成本。

本文专注于 Surface Memory Tiling,先比较 Linear 和 Tiled 布局,再从地址生成器的角度拆解 Intel X/Y/Yf/Tile4/Tile64 等布局背后的共同原理,并提供两个只依赖 C++17 标准库的实验:

  1. 按固定随机顺序访问二维块,对比 Linear 与 Tiled 的缓存和页局部性。
  2. 模拟旋转 90° 后的 3×3 邻域读取,观察图像类访问中的布局差异。

完整代码:


1. Linear 布局:简单,但只照顾一个方向

像素坐标 (x, y) 从左上角 (0, 0) 开始。设 Base 是图像首地址,$B$ 是每像素字节数,$Pitch$ 是相邻两行首地址之间的距离;RGBA8 中 $B=4$,而 $Pitch$ 可能包含行尾对齐产生的 padding。

\[Address_{linear}(x,y)=Base+y\times Pitch+x\times B\]

$y\times Pitch$ 跳过前面的存储行,$x\times B$ 再定位到当前行中的像素,结果是该像素第一个字节的地址。

3840 × 2160 的 RGBA8 图像为例,每像素 4 字节,不考虑额外对齐时:

1
2
3
4
5
pitch = 3840 × 4 = 15360 bytes

(x, y)     -> base + y × 15360 + x × 4
(x+1, y)   -> 与前者相差 4 bytes
(x, y+1)   -> 与前者相差 15360 bytes

它对“从左到右扫描整行”非常友好,CPU 也容易预取。但 GPU 很少只做这种访问:

  • Pixel Shader 通常以 2×2 Quad 工作;
  • 双线性过滤至少需要一个 2×2 texel 邻域;
  • 各向异性过滤会沿不同方向读取更多 texel;
  • 深度测试、颜色压缩和清除通常按二维块管理;
  • 图像旋转、卷积、透视变换会产生纵向、斜向或不规则访问。

在 Linear 布局中,横向邻居很近,纵向邻居却可能相隔十几 KB。相邻 GPU lane 一旦沿 Y 方向取样,请求就容易分散到不同的缓存行和内存页。

2. Tiling 的底层本质:把 X、Y 坐标位放进地址低位

Tiling 仍然要回答“像素 (x, y) 在哪个字节地址”这个问题,只是寻址被拆成两步:先找到像素属于哪个 Tile,再计算它在 Tile 内部的位置。

Linear 按行存储与 Tiled 按二维块存储的对比 图中使用简化的 8×8 图像和 4×4 Tile。橙色单格是后文计算的逻辑像素 (2,2);绿色粗线是 Tile 边界。实际 Intel 布局还会继续重排 Tile 内的地址位。

图中格子里的数字是像素对应的内存地址序号,以一个像素为单位,不是像素的颜色值。左右两侧使用相同的逻辑图像坐标;数字不同,是因为同一个像素在两种布局中的物理地址不同。

Intel 通常按字节定义 Tile 宽度。若每像素占 $B$ 字节,像素横坐标 $x$ 对应的字节横坐标记为 $x_b$:

\[x_b=x\times B\]

设 Tile 宽 $W_b$ 字节、高 $H$ 行。整除得到它在 Tile 网格中的编号;$\lfloor\ \rfloor$ 表示向下取整:

\[TileX=\left\lfloor\frac{x_b}{W_b}\right\rfloor,\qquad TileY=\left\lfloor\frac{y}{H}\right\rfloor\]

取模则得到 Tile 内坐标,其中 $LocalX$ 以字节计,$LocalY$ 以行计:

\[LocalX=x_b\bmod W_b,\qquad LocalY=y\bmod H\]

设每个 Tile 占 $S=W_b\times H$ 字节,每个物理行有 $N$ 个 Tile。$P(LocalX,LocalY)$ 表示局部坐标经过 Tile 内部排列后得到的字节偏移,最终地址为:

\[Address_{tiled}=Base+(TileY\times N+TileX)\times S+P(LocalX,LocalY)\]

最简单的 $P$ 是 LocalY × Wb + LocalX,即 Tile 内仍按行存储。直接用图中橙色标出的逻辑像素 (x=2, y=2) 计算:图像是 8×8 RGBA8,因此 $B=4B$、$Pitch=32B$;每个 Tile 为 4×4 像素,因此 $W_b=16B$、$H=4$、$N=2$、$S=64B$。以下地址都用相对 Base 的偏移表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
逻辑坐标:x = 2, y = 2
x_b = x × B = 2 × 4 = 8B

Linear:offset = y × Pitch + x_b
               = 2 × 32 + 8
               = 72B = 第 18 个像素位置    ← 左图中的 18

Tiled: TileX  = floor(8 / 16) = 0
        TileY  = floor(2 / 4)  = 0
        LocalX = 8 mod 16      = 8B
        LocalY = 2 mod 4       = 2

        tile_base = (TileY × N + TileX) × S
                  = (0 × 2 + 0) × 64 = 0B
        P          = LocalY × W_b + LocalX
                  = 2 × 16 + 8 = 40B
        offset     = tile_base + P
                  = 40B = 第 10 个像素位置  ← 右图中的 10

x=2 始终表示图片中从左起第 3 列,$x_b=8B$ 只是这个逻辑横坐标的字节形式。图中的简化 Tiled 布局在 Tile 内仍按行排列;Intel Y-Tiling、Yf、Tile4 和 Tile64 改变的是 $P$ 以及 Tile 的分层方式,外层仍然先确定像素属于哪个 Tile。

从地址位的角度看,这类布局可以抽象为:

1
2
高地址位                                      低地址位
[ TileY ][ TileX ][ local X 高位 ][ local Y ][ local X 低位 ][ byte ]

X、Y 坐标的低位同时参与低地址位寻址,二维小邻域因而集中在较窄的地址范围内。采样时也不需要搬动整张图:Sampler、Render Cache 或 Load/Store 单元中的地址生成器读取 Surface State 中的 base、pitch、格式和 tiling mode,直接把 (x, y, sample, mip, plane) 组合成 GPU 虚拟地址。固定布局的计算主要由掩码、移位、拼接和 XOR 完成,很适合做成硬件逻辑。

3. Intel X、Y、Yf、Tile4 与 Tile64 布局

这些名称描述的不是“把图像切成多大的像素块”,而是从逻辑坐标到物理地址的完整排列规则。规则至少包含两层:主 Tile 如何铺满 Surface,以及 Tile 内的 Cache Line、子块如何排列。Intel 通常用字节和行描述物理形状,换算成像素后的宽高还取决于每个元素的位数。

3.1 Format Modifier:共享缓冲的布局协议

FourCC 只回答“一个像素有哪些通道、每个通道多少位、各 plane 如何表示”。它没有说明这些像素在内存中是 Linear、X、Y、Yf、Tile4、Tile64,是否启用压缩,也没有说明 CCS 辅助数据放在哪里。Format modifier 是一个带厂商命名空间的 64 位标识符,它和 FourCC 共同确定缓冲区布局。 Linux DRM 要求把二者视为唯一的 fourcc:modifier 对;上层程序通常只比较和协商这个不透明值,不解析其中的位。DRM Format Modifiers

例如,同样是 XRGB8888:

1
2
3
DRM_FORMAT_XRGB8888 : DRM_FORMAT_MOD_LINEAR
DRM_FORMAT_XRGB8888 : I915_FORMAT_MOD_Y_TILED
DRM_FORMAT_XRGB8888 : I915_FORMAT_MOD_4_TILED

三者的颜色含义相同,坐标到地址的映射不同。DRM_FORMAT_MOD_LINEAR 明确表示线性布局;“API 没有提供 modifier”并不等价于 Linear,因为旧式隐式路径可能让驱动根据内部状态选择 Tiled Surface。DRM_FORMAT_MOD_LINEAR 定义

一个可正确解释的共享 Surface 至少需要以下信息:

元数据回答的问题
widthheight有效图像区域有多大
FourCC像素格式、通道和基础 plane 关系是什么
modifier每个坐标如何映射到内存,是否包含压缩或辅助数据
每个 plane 的 offsetpitchplane 从哪里开始,跨一行或 Tile 行要跳多少字节

接下来逐一看 Intel 的具体内存布局。硬件支持的布局不一定都有可对外共享的 DRM modifier。

X、Y、Yf 和 Tile4 以 4KB 为主,Tile64 则把主 Tile 扩展到 64KB。前四种可从 Linux DRM modifier 定义观察,Tile64 的 Surface 规则由 Intel PRM 和驱动内部布局共同描述。Intel framebuffer modifiers

布局主 TileTile 内排列直观特征
X-Tiling常见为 512B × 8 行字节仍按行递增偏重横向连续访问
Y-Tiling128B × 32 行16B OWORD 沿列方向组织把小型二维邻域放入同一 Cache Line
Yf-Tiling4KB,像素形状随格式变化64B、256B、1KB、4KB 分层组织比传统 Y 更细致地保持二维局部性
Tile4128B × 32 行64B 层与 Y 相同,中间层更横向新一代 4KB 布局;不是 4×4 像素 Tile
Tile6464KB,包含 16 个 4KB 子 Tile根据 bpe 调整子 Tile 阵列形状,并支持 mip tail新平台的 standard tiling 与大资源布局

这张表采用现代驱动用于互操作的描述。Gen2 的 X/Y Tile 是 2KB;更老平台的 Tile stride 和 bit-6 swizzle 还与具体芯片、内存配置有关,不能用同一条地址公式跨代解释。Linux UAPI 对 X/Y modifier 的可移植描述也明确限定在 Gen8+ 和 Valleyview,早期平台只能结合具体硬件处理。X/Y modifier 的平台限制

3.2 X-Tiling:Tile 内仍是小型 Linear 图像

X-Tiling 的主 Tile 和 Tile 内字节都按 row-major 排列。以常见的 512B × 8 行 形状为例,RGBA8 每行可以容纳 128 个像素,一个 Tile 对应 128×8 像素。Tile 内偏移为:

1
2
local_x_bytes = local_x × bytes_per_pixel
offset        = local_y × 512 + local_x_bytes

X-Tiling 中 64B Cache Line 的地址顺序 RGBA8 X-Tile 的 64B Cache Line 按行递增。图中的 (18,3) 位于 Cache Line 0x640;Tile 内偏移为 3×512 + 18×4 = 0x648

纵向移动在 X-Tile 内只跨越 512B,不再跨越整张图的 image pitch。不过,大部分连续地址仍留给 X 方向,所以 X-Tiling 适合较长的横向访问,小型二维采样的局部性通常不如 Y。Mesa 的 Intel Surface Layout 文档也使用了这个 RGBA8 128×8 示例。Intel Surface Layout: X-tiling

3.3 Y-Tiling:把一个 Cache Line 变成二维区域

常见 Y-Tile 为 128B × 32 行 = 4096B。它不再把整行连续放完,而是先组织 16B OWORD。一个 64B Cache Line 包含 16B × 4 行;对 RGBA8 来说,这正好是 4×4 像素,而不是 Linear 布局中的 16×1 像素。

下面的 xy 都是 RGBA8 像素在 Tile 内的坐标,范围为 0~31;offset 是像素首字节相对 Tile 起点的位置:

1
2
3
offset = (x >> 2) × 512  // 选择一条 4 像素宽、32 行高的竖条
       + y × 16          // 选择竖条中的一行
       + (x & 3) × 4     // 选择 16B 行片段中的像素

x >> 2floor(x / 4)x & 3x mod 4。例如 (x=2, y=3) 的偏移是 0×512 + 3×16 + 2×4 = 56B,仍在 Tile 的第一个 64B Cache Line 中。

Y-Tiling 的 Cache Line 地址网格与 4×4 像素展开 左图每格是一条 64B Cache Line,数字是它相对 Tile 起点的地址;右图把第一条 Cache Line 展开为 4×4 个 RGBA8 像素,与 (2,3) → 56B 的正文计算对应。

对应的 12 个 Tile 内地址位为:

1
2
高位                                                   低位
[ x4 x3 x2 ][ y4 y3 y2 y1 y0 ][ x1 x0 ][ byte1 byte0 ]

最右侧两位选择 RGBA8 像素内的字节,接着两位选择 4 像素宽度,再往左是 Y 坐标。X、Y 低位都进入低地址位后,一个 4×4 邻域便落在同一条 64B Cache Line 中。整个 4KB Y-Tile 包含 8×8 条这样的 Cache Line,并按 Y-major 顺序排列。Mesa 对 Y-Tiling 的 Cache Line 分解

旧平台还可能把较高地址位 XOR 到地址 bit 6,以改变内存通道选择。这个 bit-6 swizzle 发生在上述 Tile 内坐标排列之上,目的是改善通道分布,不应与“把 X、Y 低位放入地址低位”混为一层。i915 Hardware Tiling and Swizzling Details

3.4 Yf-Tiling:从 64B 到 4KB 的分层结构

Yf 仍使用 4KB 主 Tile,但不再只用传统 Y 的“16B 竖条”描述全部内容。内核定义把一个 Yf Tile 分成 16 个 256B 子块:

  1. 一个 64B 基本块包含 16B × 4 行
  2. 四个 64B 块组成一个 256B 子块;
  3. 四个 256B 子块以 2×2 column-major 组成一组;
  4. 四组再以 2×2 column-major 填满 4KB Tile。

Yf-Tiling 的 4KB、1KB、256B 与 64B 分层结构 RGBA8 示例中,一个 256B unit 对应 8×8 像素。逻辑坐标 (10,6) 位于横向第 2 个 unit、纵向第 1 个 unit;按 column-major 编号,它是 unit 2,起始偏移为 0x200

从 64B Cache Line、256B 数据块到整个 4KB 页面,每一层都保留二维形状。像素宽高由像素位数、采样数和 Surface 类型共同决定,不能给 Yf 设定一个跨格式不变的像素尺寸。Linux UAPI 也注明,64B 块的像素宽度会随 pixel depth 改变。I915_FORMAT_MOD_Yf_TILED 定义

3.5 Tile4:保留 Y 的外形,调整中间层

Tile4 的名字不表示 4×4 像素。它仍是 4KB Tile,整体形状仍为 128B × 32 行;64B 基本块也仍是 16B × 4 行。它和 Y 的区别集中在两者之间的分组层级:内核定义用更横向的 64B × 8 行 区域描述 Tile4,而 Y 在对应层级呈现为更窄、更高的 16B × 32 行 区域。I915_FORMAT_MOD_4_TILED 定义

Tile4 的 Cache Line 地址顺序及其与 Y 的 256B 形状对比 RGBA8 坐标 (18,6) 落在图中起始地址为 0x300 的 Cache Line。局部坐标是 (2,2),因此像素首字节偏移为 0x300 + 2×16 + 2×4 = 0x328

Tile4 保留了 Y-Tiling 的 4KB 页面边界和 64B 二维基本块,只改动中间地址位的组合方式。DG2、Meteor Lake、Lunar Lake 和 Battlemage 的压缩 modifier 都以 Tile4 为主表面,但 CCS 的存放方式不同,不能用一个笼统的 TILED 标志代替 modifier。

3.6 Tile64:面向新平台的 64KB Standard Tiling

Tile64 不是“把 Tile4 的地址连续放大 16 倍”。Intel Arc 与 Data Center GPU Flex PRM 将它定义为一个包含 16 个 4KB 子 Tile 的 64KB Tile;子 Tile 阵列和最终像素形状随 bits per element(bpe)变化,目标是让二维区域尽量接近方形。Tile64 的基地址通常要求 64KB 对齐,而 Tile4 等 4KB 模式通常要求 4KB 对齐。Intel PRM:Tile4 与 Tile64

对单采样 2D Surface,PRM 的 Tile64 地址位表可以换算出以下典型形状。物理形状使用“每行字节数 × 行数”,像素形状再除以每像素字节数:Tile64 64KB Address Swizzle

bpe4KB 子 Tile 阵列物理形状像素形状
82×8256B × 256 行256×256
164×4512B × 128 行256×128
324×4512B × 128 行128×128
648×21024B × 64 行128×64
1288×21024B × 64 行64×64

RGBA8 是 32bpe,一个 Tile64 对应 128×128 像素,也就是 4×432×32 Tile4。换成 R8 后,形状变为 256×256 像素。Tile64 中的“64”指 64KB 容量,与像素宽高无关。

Tile64 的 4×4 个 Tile4 子块及坐标分解实例 RGBA8 坐标 (70,36) 位于 Tile64 内的子 Tile (2,1),子 Tile 内坐标为 (6,4)。图中只表示逻辑分块;Tile64 与 Xe2 Tile64 的内部地址位映射不能混用。

Tile64 还支持 standard tiling 的 mip tail。当 mip level 已经很小时,多个 mip 会按规定的 slot 打包进同一个 64KB Tile,减少各自占用完整 Tile 带来的对齐浪费,并与稀疏资源的 64KB 映射粒度配合。此时 Tile64 参与的不只是二维寻址,还包括纹理层级和虚拟内存布局。Tile64 mip tail slot

新平台还在继续调整 Tile64。Mesa Intel Surface Layout 同时保留 ISL_TILING_64 与独立的 ISL_TILING_64_XE2;二者容量都是 64KB,但驱动必须使用不同的地址映射描述,不能只看“64KB”就认为布局兼容。Mesa ISL Tile64 与 Xe2 Tile64

3.7 W 与 Ys:专用布局和上一代 64KB 命名

驱动内部支持的布局比常见 DRM modifier 更多:

  • W 是历史上的特殊布局,主要服务于 stencil,并不是通用颜色缓冲布局;
  • Yf/Ys 是较早的 standard tiling 组合,f 表示 4KB,s 表示 64KB;
  • Ys 同样由 4×4 个 Yf 4KB 块组成;Tile64 是新一代 64KB standard tiling,不能仅凭容量与 Ys 互换。

这些硬件或驱动内部的 tiling mode 不保证都有独立的 I915_FORMAT_MOD_* 供 dma-buf 共享。应用不应根据 GPU 代际自行猜测布局,而应使用 API 返回的格式与 modifier 组合。

4. 性能收益与代价:Tiled 何时更快

4.1 更少的缓存行

Intel 的 OpenCL 优化资料指出,GPU L3 Cache Line 为 64B;同一硬件线程中的多个 work-item 访问同一行时,请求可以合并。源码中的 load 次数并不能直接反映带宽开销,实际触及的缓存行数量更关键。Memory Access Overview

Linear 布局只保证 X 邻域集中。Tiled 布局让一部分 X、Y 低位都参与缓存行内寻址,因此 2×24×4、旋转和斜向采样更有机会复用已经取回的数据。

4.2 更低的 TLB 和页表压力

假设处理一个按 Tile 边界对齐的 32×32 RGBA8 区域:

  • 在 Y、Yf 或 Tile4 的 RGBA8 布局中,它可以完整地落入一个连续 4KB Tile;
  • 在宽图像的 Linear 布局中,32 行之间由完整 pitch 隔开,可能触及大量不同页面。

GPU 也要做虚拟地址翻译。二维局部区域落在更少页面里,可以降低 TLB miss 和页表遍历压力。这一项在大分辨率、随机处理小块或多 surface 并发时尤其明显。

4.3 更适合内存 bank/channel 分布

规则的 stride 容易反复命中同一组 bank。Intel 一些代际会继续对地址位做 XOR swizzle,把 X、Y 方向的访问分散到不同内存通道。因此,Tiling 影响的不只有 Cache,也包括后端内存系统的并行度。

4.4 为压缩和 Fast Clear 提供固定块边界

颜色与深度压缩需要按固定区域记录状态。Intel CCS(Color Control Surface)可以记录主表面某些块是否压缩、是否可由一个 clear color 展开。固定 Tile/Cache Block 边界让压缩、部分写入和快速清除更容易由硬件追踪;CCS 在共享布局中的位置由 3.1 节定义、6.2 节实际协商的 modifier 一并确定。

压缩带来的收益仍然是带宽:如果一个块可用更少数据表示,GPU 就不必把完整颜色块写入或读回内存。

4.5 代价与适用边界

Tiling 的代价主要出现在 CPU 和跨设备边界:

  • CPU 逐行访问 Linear Surface 很自然,读取 Tiled Surface 却需要 detile 或复杂地址计算;
  • 图像宽高要按 Tile 对齐,边缘可能有额外容量;
  • 不支持某个 modifier 的显示、视频或外部设备不能直接消费该表面;
  • Linear 和 Tiled 之间的复制会消耗 Copy/Blitter 带宽;
  • 对严格 row-major 的 GPU 算法,线性 Buffer 可能延迟更低、吞吐更高。

Intel 的 OpenCL 指南明确提醒:规则的 row-major 访问通常优先使用 Buffer,image2d_t 更适合插值、边界处理、对角线或其他不规则访问。Using Buffers and Images Appropriately

CPU Mapping 也体现了这种边界:Intel 文档说明,仅包含 Intel Graphics 设备的 OpenCL Context 中,Image Mapping 效率较低,因为图像是 tiled,不能直接按线性指针映射。Mapping Memory Objects

Tiled 用更复杂的地址布局换取二维局部性;访问模式越像二维邻域,收益越可能超过地址生成和互操作成本。

5. 两个可运行的 C++ 局部性实验

两个程序只使用 C++17 标准库,在 CPU 上构造内容相同的 Linear 与简化 Tiled 图像。测试只改变物理排列,保持逻辑访问和读取量一致;布局转换不计时,结果通过 checksum 校验。它们用于观察内存局部性,不代表 Intel GPU 的硬件性能。

代码是完整单文件程序。使用支持 C++17 的 MSVC、GCC 或 Clang 直接编译即可;编译命令不是实验重点。两份 .cpp 同时保存在网站仓库的 assets/code/intel-gpu-tiling/ 目录中,可以通过正文链接直接打开或保存。

5.1 本机测试环境与计时方法

以下结果于 2026 年 8 月 9 日在同一台机器上重新编译、重新测试:

项目测试环境
操作系统Windows 11 专业版 Insider Preview,build 26220
CPUIntel Core i5-12600K,10 核 16 线程
内存32GB(2×16GB,DDR4-3600 / 3600 MT/s)
编译器MSVC 19.50.35730,x64
编译选项/O2 /EHsc /std:c++17

每个程序独立运行三次。实验一的每次运行内部包含 7 轮计时,实验二包含 5 轮,程序先预热再报告内部中位数;下文展示第二次独立运行的完整输出,并同时给出三次运行的加速比范围。测试没有固定 CPU 频率或进程亲和性,因此范围比单个最好成绩更有参考价值。

5.2 实验一:随机访问二维 Tile

图像大小为 4096×2048,像素类型为 uint32_t,每种布局占 32MiB。简化 Tiled 布局把每个 32×32 区域连续存放,因此一个 Tile 正好是 4KiB。

程序以固定随机顺序访问所有逻辑 Tile。Linear 路径中,一个 Tile 的相邻行相隔完整 image width;Tiled 路径中,Tile 的 32 行位于同一个连续 4KiB 区域。交替两种布局的运行顺序可以降低“总是先运行的一方”带来的缓存和频率偏差,最终取七轮中位数。

运行结果

1
2
3
4
5
6
Image: 4096x2048, tile: 32x32, 32.0 MiB per layout
Workload: visit every logical tile in a fixed random order, 6 pass(es) per sample
linear : median 35.626 ms, 5.26 GiB/s
tiled  : median 12.742 ms, 14.72 GiB/s
speedup: 2.80x
checksum: 1649242896384

三次独立运行的加速比为 2.76~2.87x。随机切换逻辑 Tile 时,Linear 路径不断在相距很远的图像行和页面之间跳转;Tiled 路径在进入一个 Tile 后连续读取 4KiB,因此 Cache 和 TLB 局部性都更稳定。

完整代码

源文件:tiling-locality-benchmark.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#include <algorithm>
#include <charconv>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <numeric>
#include <random>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>

namespace {

struct Options {
    std::size_t width = 4096;
    std::size_t height = 2048;
    std::size_t tile = 32;
    std::size_t passes = 6;
    std::size_t rounds = 7;
};

struct TileQuery {
    std::size_t linear_x;
    std::size_t linear_y;
    std::size_t tiled_base;
};

struct TimedResult {
    double milliseconds;
    std::uint64_t checksum;
};

[[noreturn]] void fail(const std::string& message) {
    throw std::runtime_error(message);
}

std::size_t parse_positive(std::string_view text, std::string_view name) {
    std::size_t value = 0;
    const char* begin = text.data();
    const char* end = begin + text.size();
    const auto result = std::from_chars(begin, end, value);
    if (result.ec != std::errc{} || result.ptr != end || value == 0) {
        fail("--" + std::string(name) + " must be a positive integer");
    }
    return value;
}

Options parse_options(int argc, char** argv) {
    Options options;

    for (int index = 1; index < argc; ++index) {
        const std::string_view argument(argv[index]);
        if (argument == "--help" || argument == "-h") {
            std::cout
                << "Usage: tiling-locality-benchmark [options]\n\n"
                << "  --width=N    image width in pixels (default 4096)\n"
                << "  --height=N   image height in pixels (default 2048)\n"
                << "  --tile=N     square tile edge in pixels (default 32)\n"
                << "  --passes=N   full-image reads per sample (default 6)\n"
                << "  --rounds=N   timed samples per layout (default 7)\n";
            std::exit(0);
        }

        const auto equals = argument.find('=');
        if (equals == std::string_view::npos || argument.size() < 2 ||
            argument.substr(0, 2) != "--") {
            fail("unknown argument: " + std::string(argument));
        }

        const std::string_view name = argument.substr(2, equals - 2);
        const std::size_t value = parse_positive(argument.substr(equals + 1), name);
        if (name == "width") {
            options.width = value;
        } else if (name == "height") {
            options.height = value;
        } else if (name == "tile") {
            options.tile = value;
        } else if (name == "passes") {
            options.passes = value;
        } else if (name == "rounds") {
            options.rounds = value;
        } else {
            fail("unknown option: --" + std::string(name));
        }
    }

    if (options.width % options.tile != 0 || options.height % options.tile != 0) {
        fail("width and height must both be multiples of tile");
    }
    if (options.width > std::numeric_limits<std::size_t>::max() / options.height) {
        fail("image dimensions overflow size_t");
    }
    return options;
}

std::vector<std::uint32_t> make_linear_image(std::size_t pixel_count) {
    std::vector<std::uint32_t> image(pixel_count);
    for (std::size_t index = 0; index < pixel_count; ++index) {
        const auto value = static_cast<std::uint32_t>(index);
        image[index] = (value * 2654435761u) >> 16;
    }
    return image;
}

std::vector<std::uint32_t> make_tiled_image(
    const std::vector<std::uint32_t>& linear,
    const Options& options)
{
    std::vector<std::uint32_t> tiled(linear.size());
    const std::size_t tiles_x = options.width / options.tile;
    const std::size_t tiles_y = options.height / options.tile;
    const std::size_t tile_area = options.tile * options.tile;

    for (std::size_t tile_y = 0; tile_y < tiles_y; ++tile_y) {
        for (std::size_t tile_x = 0; tile_x < tiles_x; ++tile_x) {
            const std::size_t tiled_base = (tile_y * tiles_x + tile_x) * tile_area;
            const std::size_t source_x = tile_x * options.tile;
            const std::size_t source_y = tile_y * options.tile;

            for (std::size_t local_y = 0; local_y < options.tile; ++local_y) {
                const std::size_t source_row =
                    (source_y + local_y) * options.width + source_x;
                const std::size_t tiled_row = tiled_base + local_y * options.tile;
                std::copy_n(
                    linear.data() + source_row,
                    options.tile,
                    tiled.data() + tiled_row);
            }
        }
    }
    return tiled;
}

std::vector<TileQuery> make_queries(const Options& options) {
    const std::size_t tiles_x = options.width / options.tile;
    const std::size_t tiles_y = options.height / options.tile;
    const std::size_t tile_area = options.tile * options.tile;
    std::vector<TileQuery> queries;
    queries.reserve(tiles_x * tiles_y);

    for (std::size_t tile_y = 0; tile_y < tiles_y; ++tile_y) {
        for (std::size_t tile_x = 0; tile_x < tiles_x; ++tile_x) {
            const std::size_t tile_index = tile_y * tiles_x + tile_x;
            queries.push_back({
                tile_x * options.tile,
                tile_y * options.tile,
                tile_index * tile_area,
            });
        }
    }

    std::mt19937 generator(0xC0FFEEu);
    std::shuffle(queries.begin(), queries.end(), generator);
    return queries;
}

std::uint64_t sum_linear_tiles(
    const std::vector<std::uint32_t>& image,
    const std::vector<TileQuery>& queries,
    const Options& options,
    std::size_t passes)
{
    std::uint64_t checksum = 0;
    for (std::size_t pass = 0; pass < passes; ++pass) {
        for (const TileQuery& query : queries) {
            for (std::size_t local_y = 0; local_y < options.tile; ++local_y) {
                const std::uint32_t* row = image.data()
                    + (query.linear_y + local_y) * options.width
                    + query.linear_x;
                for (std::size_t local_x = 0; local_x < options.tile; ++local_x) {
                    checksum += row[local_x];
                }
            }
        }
    }
    return checksum;
}

std::uint64_t sum_tiled_tiles(
    const std::vector<std::uint32_t>& image,
    const std::vector<TileQuery>& queries,
    const Options& options,
    std::size_t passes)
{
    std::uint64_t checksum = 0;
    for (std::size_t pass = 0; pass < passes; ++pass) {
        for (const TileQuery& query : queries) {
            for (std::size_t local_y = 0; local_y < options.tile; ++local_y) {
                const std::uint32_t* row = image.data()
                    + query.tiled_base
                    + local_y * options.tile;
                for (std::size_t local_x = 0; local_x < options.tile; ++local_x) {
                    checksum += row[local_x];
                }
            }
        }
    }
    return checksum;
}

template <typename Operation>
TimedResult measure(Operation&& operation) {
    const auto start = std::chrono::steady_clock::now();
    const std::uint64_t checksum = operation();
    const auto finish = std::chrono::steady_clock::now();
    const std::chrono::duration<double, std::milli> elapsed = finish - start;
    return {elapsed.count(), checksum};
}

double median(std::vector<double> values) {
    std::sort(values.begin(), values.end());
    const std::size_t middle = values.size() / 2;
    if (values.size() % 2 == 0) {
        return (values[middle - 1] + values[middle]) / 2.0;
    }
    return values[middle];
}

void print_result(
    std::string_view name,
    const std::vector<double>& samples,
    double bytes_read)
{
    const double middle = median(samples);
    const double gib_per_second = bytes_read / (middle / 1000.0) / (1024.0 * 1024.0 * 1024.0);
    std::cout << std::left << std::setw(7) << name << std::right
              << ": median " << std::fixed << std::setprecision(3) << middle << " ms, "
              << std::setprecision(2) << gib_per_second << " GiB/s\n";
}

} // namespace

int main(int argc, char** argv) {
    try {
        const Options options = parse_options(argc, argv);
        const std::size_t pixel_count = options.width * options.height;
        const double bytes_per_layout =
            static_cast<double>(pixel_count * sizeof(std::uint32_t));

        std::cout << "Image: " << options.width << 'x' << options.height
                  << ", tile: " << options.tile << 'x' << options.tile
                  << ", " << std::fixed << std::setprecision(1)
                  << bytes_per_layout / (1024.0 * 1024.0) << " MiB per layout\n"
                  << "Workload: visit every logical tile in a fixed random order, "
                  << options.passes << " pass(es) per sample\n";

        const std::vector<std::uint32_t> linear = make_linear_image(pixel_count);
        const std::vector<std::uint32_t> tiled = make_tiled_image(linear, options);
        const std::vector<TileQuery> queries = make_queries(options);

        const std::uint64_t warm_linear =
            sum_linear_tiles(linear, queries, options, 1);
        const std::uint64_t warm_tiled =
            sum_tiled_tiles(tiled, queries, options, 1);
        if (warm_linear != warm_tiled) {
            fail("layout conversion failed: warm-up checksums differ");
        }

        std::vector<double> linear_samples;
        std::vector<double> tiled_samples;
        linear_samples.reserve(options.rounds);
        tiled_samples.reserve(options.rounds);
        std::uint64_t linear_checksum = 0;
        std::uint64_t tiled_checksum = 0;

        for (std::size_t round = 0; round < options.rounds; ++round) {
            const auto run_linear = [&] {
                const TimedResult result = measure([&] {
                    return sum_linear_tiles(linear, queries, options, options.passes);
                });
                linear_samples.push_back(result.milliseconds);
                linear_checksum = result.checksum;
            };
            const auto run_tiled = [&] {
                const TimedResult result = measure([&] {
                    return sum_tiled_tiles(tiled, queries, options, options.passes);
                });
                tiled_samples.push_back(result.milliseconds);
                tiled_checksum = result.checksum;
            };

            if (round % 2 == 0) {
                run_linear();
                run_tiled();
            } else {
                run_tiled();
                run_linear();
            }
        }

        if (linear_checksum != tiled_checksum) {
            fail("measured checksums differ");
        }

        const double bytes_read = bytes_per_layout * static_cast<double>(options.passes);
        print_result("linear", linear_samples, bytes_read);
        print_result("tiled", tiled_samples, bytes_read);
        const double speedup = median(linear_samples) / median(tiled_samples);
        std::cout << "speedup: " << std::fixed << std::setprecision(2)
                  << speedup << "x\n"
                  << "checksum: " << linear_checksum << "\n"
                  << "Note: this models 2D locality on a CPU; it is not a direct "
                  << "Intel GPU hardware benchmark.\n";
        return 0;
    } catch (const std::exception& exception) {
        std::cerr << "error: " << exception.what() << '\n';
        return 1;
    }
}

5.3 实验二:旋转 90°后的 3×3 邻域读取

第二个程序沿用相同的两种布局,但改为图像类访问:输出坐标横向移动时,源坐标沿 Y 方向移动,并为每个中心点读取 3×3 邻域。在 Linear 图像中,相邻中心点之间跨越完整 pitch;在 Tiled 图像中,纵向邻居通常仍在同一个 4KiB Tile 内。

为了避免把除法、取模等 C++ 地址计算开销误认为布局性能,程序提前把地址拆成 X、Y 两部分。计时循环对两种布局调用同一个 sum_rotated_stencil(),每次像素读取都只计算 y_part[y] + x_part[x]

运行结果

1
2
3
4
5
6
Source: 4096x2048, tile: 32x32, 32.0 MiB per layout
Workload: 90-degree rotated 3x3 source reads, 1 pass(es) per sample
linear : median 75.387 ms, 3.73 logical GiB/s
tiled  : median 26.788 ms, 10.48 logical GiB/s
speedup: 2.81x
checksum: 2470241170044

logical GiB/s 按每个输出点的 9 次像素读取计算。邻域之间存在重叠和 Cache 命中,因此它不等于 DRAM 实际传输量。三次独立运行的加速比为 2.81~3.12x

完整代码

源文件:tiling-rotated-stencil-benchmark.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#include <algorithm>
#include <charconv>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>

namespace {

struct Options {
    std::size_t width = 4096;
    std::size_t height = 2048;
    std::size_t tile = 32;
    std::size_t passes = 1;
    std::size_t rounds = 5;
};

struct AddressParts {
    std::vector<std::size_t> x;
    std::vector<std::size_t> y;
};

struct TimedResult {
    double milliseconds;
    std::uint64_t checksum;
};

[[noreturn]] void fail(const std::string& message) {
    throw std::runtime_error(message);
}

std::size_t parse_positive(std::string_view text, std::string_view name) {
    std::size_t value = 0;
    const char* begin = text.data();
    const char* end = begin + text.size();
    const auto result = std::from_chars(begin, end, value);
    if (result.ec != std::errc{} || result.ptr != end || value == 0) {
        fail("--" + std::string(name) + " must be a positive integer");
    }
    return value;
}

Options parse_options(int argc, char** argv) {
    Options options;

    for (int index = 1; index < argc; ++index) {
        const std::string_view argument(argv[index]);
        if (argument == "--help" || argument == "-h") {
            std::cout
                << "Usage: tiling-rotated-stencil-benchmark [options]\n\n"
                << "  --width=N    source width in pixels (default 4096)\n"
                << "  --height=N   source height in pixels (default 2048)\n"
                << "  --tile=N     square tile edge in pixels (default 32)\n"
                << "  --passes=N   rotated 3x3 reads per sample (default 1)\n"
                << "  --rounds=N   timed samples per layout (default 5)\n";
            std::exit(0);
        }

        const auto equals = argument.find('=');
        if (equals == std::string_view::npos || argument.size() < 2 ||
            argument.substr(0, 2) != "--") {
            fail("unknown argument: " + std::string(argument));
        }

        const std::string_view name = argument.substr(2, equals - 2);
        const std::size_t value = parse_positive(argument.substr(equals + 1), name);
        if (name == "width") {
            options.width = value;
        } else if (name == "height") {
            options.height = value;
        } else if (name == "tile") {
            options.tile = value;
        } else if (name == "passes") {
            options.passes = value;
        } else if (name == "rounds") {
            options.rounds = value;
        } else {
            fail("unknown option: --" + std::string(name));
        }
    }

    if (options.width < 3 || options.height < 3) {
        fail("width and height must both be at least 3");
    }
    if (options.width % options.tile != 0 || options.height % options.tile != 0) {
        fail("width and height must both be multiples of tile");
    }
    if (options.width > std::numeric_limits<std::size_t>::max() / options.height) {
        fail("image dimensions overflow size_t");
    }
    return options;
}

std::vector<std::uint32_t> make_linear_image(std::size_t pixel_count) {
    std::vector<std::uint32_t> image(pixel_count);
    for (std::size_t index = 0; index < pixel_count; ++index) {
        const auto value = static_cast<std::uint32_t>(index);
        image[index] = (value * 2654435761u) >> 16;
    }
    return image;
}

std::vector<std::uint32_t> make_tiled_image(
    const std::vector<std::uint32_t>& linear,
    const Options& options)
{
    std::vector<std::uint32_t> tiled(linear.size());
    const std::size_t tiles_x = options.width / options.tile;
    const std::size_t tiles_y = options.height / options.tile;
    const std::size_t tile_area = options.tile * options.tile;

    for (std::size_t tile_y = 0; tile_y < tiles_y; ++tile_y) {
        for (std::size_t tile_x = 0; tile_x < tiles_x; ++tile_x) {
            const std::size_t tiled_base = (tile_y * tiles_x + tile_x) * tile_area;
            const std::size_t source_x = tile_x * options.tile;
            const std::size_t source_y = tile_y * options.tile;

            for (std::size_t local_y = 0; local_y < options.tile; ++local_y) {
                const std::size_t source_row =
                    (source_y + local_y) * options.width + source_x;
                const std::size_t tiled_row = tiled_base + local_y * options.tile;
                std::copy_n(
                    linear.data() + source_row,
                    options.tile,
                    tiled.data() + tiled_row);
            }
        }
    }
    return tiled;
}

AddressParts make_linear_parts(const Options& options) {
    AddressParts parts;
    parts.x.resize(options.width);
    parts.y.resize(options.height);
    for (std::size_t x = 0; x < options.width; ++x) {
        parts.x[x] = x;
    }
    for (std::size_t y = 0; y < options.height; ++y) {
        parts.y[y] = y * options.width;
    }
    return parts;
}

AddressParts make_tiled_parts(const Options& options) {
    AddressParts parts;
    parts.x.resize(options.width);
    parts.y.resize(options.height);
    const std::size_t tiles_x = options.width / options.tile;
    const std::size_t tile_area = options.tile * options.tile;

    for (std::size_t x = 0; x < options.width; ++x) {
        parts.x[x] = (x / options.tile) * tile_area + x % options.tile;
    }
    for (std::size_t y = 0; y < options.height; ++y) {
        parts.y[y] =
            (y / options.tile) * tiles_x * tile_area
            + (y % options.tile) * options.tile;
    }
    return parts;
}

std::uint64_t sum_rotated_stencil(
    const std::vector<std::uint32_t>& image,
    const AddressParts& address,
    const Options& options,
    std::size_t passes)
{
    std::uint64_t checksum = 0;

    // A 90-degree rotation swaps the output dimensions.  Moving along an
    // output row therefore walks vertically through the source image.
    for (std::size_t pass = 0; pass < passes; ++pass) {
        for (std::size_t output_y = 1; output_y + 1 < options.width; ++output_y) {
            const std::size_t source_x = output_y;
            const std::size_t x0 = address.x[source_x - 1];
            const std::size_t x1 = address.x[source_x];
            const std::size_t x2 = address.x[source_x + 1];

            for (std::size_t output_x = 1; output_x + 1 < options.height; ++output_x) {
                const std::size_t source_y = options.height - 1 - output_x;
                const std::size_t y0 = address.y[source_y - 1];
                const std::size_t y1 = address.y[source_y];
                const std::size_t y2 = address.y[source_y + 1];

                checksum += image[y0 + x0] + image[y0 + x1] + image[y0 + x2];
                checksum += image[y1 + x0] + image[y1 + x1] + image[y1 + x2];
                checksum += image[y2 + x0] + image[y2 + x1] + image[y2 + x2];
            }
        }
    }
    return checksum;
}

template <typename Operation>
TimedResult measure(Operation&& operation) {
    const auto start = std::chrono::steady_clock::now();
    const std::uint64_t checksum = operation();
    const auto finish = std::chrono::steady_clock::now();
    const std::chrono::duration<double, std::milli> elapsed = finish - start;
    return {elapsed.count(), checksum};
}

double median(std::vector<double> values) {
    std::sort(values.begin(), values.end());
    const std::size_t middle = values.size() / 2;
    if (values.size() % 2 == 0) {
        return (values[middle - 1] + values[middle]) / 2.0;
    }
    return values[middle];
}

void print_result(
    std::string_view name,
    const std::vector<double>& samples,
    double logical_bytes_read)
{
    const double middle = median(samples);
    const double gib_per_second =
        logical_bytes_read / (middle / 1000.0) / (1024.0 * 1024.0 * 1024.0);
    std::cout << std::left << std::setw(7) << name << std::right
              << ": median " << std::fixed << std::setprecision(3) << middle << " ms, "
              << std::setprecision(2) << gib_per_second << " logical GiB/s\n";
}

} // namespace

int main(int argc, char** argv) {
    try {
        const Options options = parse_options(argc, argv);
        const std::size_t pixel_count = options.width * options.height;
        const double bytes_per_layout =
            static_cast<double>(pixel_count * sizeof(std::uint32_t));
        const std::size_t output_pixels =
            (options.width - 2) * (options.height - 2);

        std::cout << "Source: " << options.width << 'x' << options.height
                  << ", tile: " << options.tile << 'x' << options.tile
                  << ", " << std::fixed << std::setprecision(1)
                  << bytes_per_layout / (1024.0 * 1024.0) << " MiB per layout\n"
                  << "Workload: 90-degree rotated 3x3 source reads, "
                  << options.passes << " pass(es) per sample\n";

        const std::vector<std::uint32_t> linear = make_linear_image(pixel_count);
        const std::vector<std::uint32_t> tiled = make_tiled_image(linear, options);
        const AddressParts linear_address = make_linear_parts(options);
        const AddressParts tiled_address = make_tiled_parts(options);

        const std::uint64_t warm_linear =
            sum_rotated_stencil(linear, linear_address, options, 1);
        const std::uint64_t warm_tiled =
            sum_rotated_stencil(tiled, tiled_address, options, 1);
        if (warm_linear != warm_tiled) {
            fail("layout conversion failed: warm-up checksums differ");
        }

        std::vector<double> linear_samples;
        std::vector<double> tiled_samples;
        linear_samples.reserve(options.rounds);
        tiled_samples.reserve(options.rounds);
        std::uint64_t linear_checksum = 0;
        std::uint64_t tiled_checksum = 0;

        for (std::size_t round = 0; round < options.rounds; ++round) {
            const auto run_linear = [&] {
                const TimedResult result = measure([&] {
                    return sum_rotated_stencil(
                        linear, linear_address, options, options.passes);
                });
                linear_samples.push_back(result.milliseconds);
                linear_checksum = result.checksum;
            };
            const auto run_tiled = [&] {
                const TimedResult result = measure([&] {
                    return sum_rotated_stencil(
                        tiled, tiled_address, options, options.passes);
                });
                tiled_samples.push_back(result.milliseconds);
                tiled_checksum = result.checksum;
            };

            if (round % 2 == 0) {
                run_linear();
                run_tiled();
            } else {
                run_tiled();
                run_linear();
            }
        }

        if (linear_checksum != tiled_checksum) {
            fail("measured checksums differ");
        }

        const double logical_bytes_read = static_cast<double>(output_pixels)
            * 9.0 * sizeof(std::uint32_t) * static_cast<double>(options.passes);
        print_result("linear", linear_samples, logical_bytes_read);
        print_result("tiled", tiled_samples, logical_bytes_read);
        const double speedup = median(linear_samples) / median(tiled_samples);
        std::cout << "speedup: " << std::fixed << std::setprecision(2)
                  << speedup << "x\n"
                  << "checksum: " << linear_checksum << "\n"
                  << "Note: this models image-style 2D reads on a CPU; it is not a "
                  << "direct Intel GPU hardware benchmark.\n";
        return 0;
    } catch (const std::exception& exception) {
        std::cerr << "error: " << exception.what() << '\n';
        return 1;
    }
}

5.4 结果应该如何解读与验证

按二维块或纵向邻域访问时,Tiled 版本把近期要用的数据集中在更少的连续块中。实验里的 32×32 Tile 内部仍按行排列,只模拟内存局部性;Intel GPU 的 Sampler Cache、CCS、硬件地址生成器以及 Yf、Tile4、Tile64 的真实映射都不在模型内。

本机测得的加速比只适用于这台 CPU 和这两种访问模式,不能直接套到 Intel GPU。实验能确认的是变化趋势:访问越偏向二维、image pitch 越大,Linear 布局的 Cache/TLB 代价越明显。

如果继续在真实 Intel GPU 上验证,只看 Kernel 时间还不够。使用 Intel GPA 或对应的性能工具时,可以关注:

  • EU ActiveEU Stall:执行单元是在计算还是等待数据;
  • Sampler Busy/Stalled:Image 路径是否被 Sampler 限制;
  • Sampler Cache Misses:纹理缓存未命中导致的外部读取量;
  • 实际 DRAM/LLC/L3 带宽;
  • 改变图像尺寸后,性能是否在越过 Cache 容量时突然变化。

Intel GPA 将 Sampler Cache Miss 以 64B 读取块统计,因此它能直接帮助判断采样模式是否反复拉取新的缓存行。Intel GPA GPU Metrics

做严谨对比时还应保证:

  1. 两条路径使用相同像素格式和数学运算;
  2. 使用 GPU Timestamp/Event,而不是只测 CPU 提交时间;
  3. 先预热 Shader、Cache 和 GPU 频率;
  4. 使用超过 LLC 容量的数据,并报告中位数而不是最好成绩;
  5. 校验输出,避免编译器消除工作或两条路径语义不同;
  6. 同时测试 row-major、纵向、旋转和随机访问,不用单一数字代表所有 workload。

6. Intel GPU 中 Tiling 的实际应用

开发者通常不手写 X/Y/Yf/Tile4/Tile64 地址公式,而是通过 API 表达用途,让驱动选择布局:

应用API 层表达Tiling 的作用
纹理采样OpenCL image2d_t、Vulkan optimal image、D3D texture改善二维和过滤采样
Render TargetColor/Depth Attachment配合 Render Cache、压缩与 Fast Clear
后处理Image/Texture ping-pong提高卷积、重投影和邻域读取局部性
视频处理NV12/P010 多 plane Surface让解码、VPP、OpenCL 和显示共享 GPU Surface
显示扫描dma-buf + format modifier在 Display Engine 支持时避免 detile copy

6.1 让原生布局贯穿处理链

工程上应尽量让 Surface 保持 GPU 原生布局。例如,视频解码得到 NV12 Surface 后,如果先拷回 CPU Linear Buffer,再上传给 OpenCL,前后两次全帧复制很可能抵消 Tiling 的收益。DirectX/OpenCL、VA-API/OpenCL 或 dma-buf 等互操作机制可以共享同一个 Surface,但必须一并传递正确的格式、plane 和 modifier 信息。

6.2 用 modifier 协商零拷贝共享

Tiled Surface 的 pitch 通常按 Tile 宽度对齐,底部也可能补齐到 Tile 高度。modifier 还可能改变 plane 数量和分配大小。例如 I915_FORMAT_MOD_Y_TILED_CCS 把主 Surface 放在 plane 0、CCS 放在 plane 1;DG2 的部分 Tile4 CCS modifier 将压缩元数据放在 GEM 对象之外的保留区域,而 Meteor Lake 的 modifier 又可以把线性 CCS 作为额外 plane。它们的主图像都可能是 Tile4,但不能作为同一种布局互换。Intel CCS modifier definitions

跨设备共享时,modifier 不是由生产者单方面指定:

1
2
3
4
5
生产者支持的 (fourcc, modifier)
                ∩
消费者支持的 (fourcc, modifier)
                ↓
选择共同组合 → 分配 Surface → 连同 dma-buf、offset、pitch 一起传递

KMS 通过 Plane 的 IN_FORMATS 属性公布组合,EGL、Vulkan 和 Wayland 也有相应的 modifier 枚举与导入机制。如果双方没有共同组合,只能退回共同支持的 Linear 布局,或增加一次 detile/retile copy。Exchanging pixel buffers

7. 总结

Intel GPU 图像 Tiling 的底层可以归结为三件事:

  1. 将 Surface 划分为固定字节大小的二维 Tile;
  2. 重新排列 X、Y 坐标低位,使二维邻域落入更少的缓存行和内存页;
  3. 在此基础上组织 Cache Block、内存通道分布、CCS 压缩和设备间共享。

Linear 布局简单、便于 CPU 访问,也适合连续扫描整行;Tiled 布局更适合二维邻域。选择布局前要先确认 workload 的访问方向,再用设备时间和 Cache/带宽指标验证。仅凭 image2d_tVK_IMAGE_TILING_OPTIMAL 无法预判性能。

Tiling 所做的事很具体:让地址布局更接近 GPU 实际处理像素的形状。

本文由作者按照 CC BY 4.0 进行授权