SIMD 加速
TGFX 在 CPU 端的性能热点路径中使用了 SIMD(Single Instruction, Multiple Data)向量化加速。与手写平台特定 intrinsics(如 SSE/NEON)不同,TGFX 基于 Google 的 Highway 库实现了跨平台 SIMD 抽象——编译时自动生成多种 ISA 版本,运行时动态选择最优实现。
Highway 动态分发机制
Highway 是 Google 开发的 C++ SIMD 库,其核心理念是「编写一次,在所有 ISA 上运行」。TGFX 的每个 SIMD 加速文件都遵循相同的代码组织模式:
编译时:多 ISA 版本生成
// ① 声明当前文件路径(foreach_target.h 需要据此重新 include 自身)
#undef HWY_TARGET_INCLUDE
#define HWY_TARGET_INCLUDE "core/RectSIMD.cpp"
// ② 根据目标平台的所有可用 ISA,多次 include 当前文件
#include "hwy/foreach_target.h"
// ③ 引入 Highway 核心 API
#include "hwy/highway.h"
// ④ 在目标命名空间中编写 SIMD 实现
HWY_BEFORE_NAMESPACE();
namespace tgfx {
namespace HWY_NAMESPACE { // 每次 include 会展开为不同的 ISA 命名空间
namespace hn = hwy::HWY_NAMESPACE;
// SIMD 实现代码...
} // namespace HWY_NAMESPACE
} // namespace tgfx
HWY_AFTER_NAMESPACE();
foreach_target.h 会根据编译目标平台的 CPU 架构,多次 #include 当前源文件。每次 include 时 HWY_NAMESPACE 展开为不同的 ISA 命名空间(如 N_SSE4、N_AVX2、N_NEON 等),从而在一个 .cpp 文件中生成多份针对不同指令集的机器码。
运行时:动态选择最优 ISA
#if HWY_ONCE // 只在最后一次 include 时编译
namespace tgfx {
// 注册所有 ISA 版本的函数指针表
HWY_EXPORT(SetBoundsHWYImpl);
// 公开 API:运行时自动分发到当前 CPU 支持的最优版本
bool Rect::setBounds(const Point pts[], int count) {
return HWY_DYNAMIC_DISPATCH(SetBoundsHWYImpl)(this, pts, count);
}
} // namespace tgfx
#endif
HWY_EXPORT 宏为每个 SIMD 函数生成一个包含所有 ISA 版本函数指针的静态表。HWY_DYNAMIC_DISPATCH 在首次调用时通过 CPUID(x86)或辅助向量(ARM)探测当前 CPU 支持的最高指令集,然后缓存选择结果,后续调用直接跳转到最优版本,没有运行时开销。
支持的指令集
| 平台 | 支持的 ISA(由高到低) |
|---|---|
| x86_64 | AVX-512 → AVX2 → SSE4.1 → SSE2 → 标量回退 |
| ARM64 | NEON(默认可用) → 标量回退 |
| ARM32 | NEON(视 CPU 而定) → 标量回退 |
| WASM | WASM SIMD → 标量回退 |
Highway 确保了即使在不支持 SIMD 的平台上也能正确运行——最差情况下回退到标量实现,功能完全一致。
加速场景一:Rect 边界计算
RectSIMD.cpp 加速了 Rect::setBounds() 方法——从一组 Point 数组中计算最小包围矩形。
加速原理
利用 128-bit 向量同时处理两个 Point(4 个 float),通过 Min/Max 指令并行归约:
输入: pts[0]=(x0,y0), pts[1]=(x1,y1), pts[2]=(x2,y2), ...
│
▼
┌─────────────────────────┐
│ 128-bit 向量: [x0, y0, x1, y1] │
│ 同时比较两个点的 x 和 y │
└─────────────────────────┘
│
Min / Max 归约
│
▼
min = [minX_lo, minY_lo, minX_hi, minY_hi]
max = [maxX_lo, maxY_lo, maxX_hi, maxY_hi]
│
▼
rect = (min(minX_lo, minX_hi),
min(minY_lo, minY_hi),
max(maxX_lo, maxX_hi),
max(maxY_lo, maxY_hi))
实现中还通过 accum = Mul(accum, xy) 累乘检测 NaN/Inf——如果任何输入值不是有限数,乘以零后不会等于零(NaN 的特性),从而用 Eq(accum*0, 0) 一步检测所有非有限值。
应用场景
Rect::setBounds() 是 Path 边界计算的底层方法,被 Path::getBounds()、Path::computeTightBounds() 等高频方法调用。在复杂路径(如 SVG 导入的数千控制点路径)中,这个优化能显著减少边界计算耗时。
加速场景二:矩阵运算
MatrixSIMD.cpp 加速了 Matrix 类的核心运算,覆盖了矩阵变换的所有情况:
| 方法 | SIMD 实现 | 加速原理 |
|---|---|---|
Matrix::TransPoints() | TransPointsHWYImpl | 交错向量 [tx, ty, tx, ty] 批量加法,一次处理 N/2 个点 |
Matrix::ScalePoints() | ScalePointsHWYImpl | MulAdd(src, [sx,sy,...], [tx,ty,...]) 融合乘加 |
Matrix::AffinePoints() | AffinePointsHWYImpl | Reverse2 交换 xy,实现 skew 乘加:src*scale + swz*skew + trans |
Matrix::PerspPoints() | PerspPointsHWYImpl | 同时计算 x'、y' 和 w,用 Div 做透视除法 |
Matrix::mapRect() | MapRectHWYImpl | 将 LTRB 视为 4-float 向量,Min/Max 直接得到变换后的包围盒 |
Matrix::ConcatMatrix() | ConcatMatrixHWYImpl | 行列向量化:ReduceSum(Mul(row, col)) 求每个结果元素 |
Matrix::mapHomogeneous() | MapHomogeneousHWYImpl | 3 列向量的 MulAdd 链 |
矩阵类型分级优化
MapRectHWYImpl 根据矩阵类型选择不同的加速路径:
- 纯平移(
TranslateMask):LTRB +[tx, ty, tx, ty],然后Min/Max确保 LTRB 顺序正确(处理负缩放) - 缩放+平移(
isScaleTranslate()):MulAdd(LTRB, [sx,sy,sx,sy], [tx,ty,tx,ty]),同样需要Min/Max规范化 - 透视(
PerspectiveMask):先用MapHomogeneousHWYImpl变换四角到齐次坐标,然后ProjectCornerWithClip处理 w 平面裁剪,最终四角Min得到包围盒 - 仿射:变换四角后调用
Rect::setBounds()归约
透视裁剪
透视变换中,某些点可能落在摄像机后方(w < 0)。MapRectPerspective 通过 ClipEdgeToW0Plane 将边与 w = 1/2^14 平面求交,避免除以接近零的 w 值导致的数值爆炸。整个裁剪过程也使用 SIMD 向量运算——交点坐标以 (x, y, -x, -y) 格式存储,利用 Min 指令同时更新包围盒的四个边界。
加速场景三:向量运算
VecSIMD.cpp 为 Vec4 类型的所有运算提供 SIMD 加速。Vec4 是 TGFX 的四分量向量类型,广泛用于 Layer 系统的属性动画计算(如 3D 合成中的齐次坐标变换)。
加速的运算
| 运算类别 | 加速方法 |
|---|---|
| 算术 | +、-、*、/、-(取负)、标量乘/除 |
| 比较 | >=(GreaterEqual)、<(LessThan) |
| 逻辑 | Or、And、Any、All、IfThenElse |
| 数学 | Abs、Sqrt、Min、Max |
每个运算都是 128-bit 固定宽度(Full128<float>),恰好容纳 4 个 float 分量,实现一条指令完成四分量运算。
无分支条件选择
IfThenElseVec4HWYImpl 实现了无分支的四分量条件选择:
const auto mask = hn::Ne(vc, hn::Zero(d)); // cond != 0 → true
hn::IfThenElse(mask, vt, ve); // true 选 t,false 选 e
这在 Layer 动画系统中用于混合计算——例如根据插值因子在两组属性值间选择,避免了标量代码中的四次分支判断。
加速场景四:Tile 排序
TileSortCompareFuncSIMD.cpp 加速了分块渲染中的 Tile 排序比较函数。
背景
TGFX 的 Layer 系统采用分块渲染策略(tiled rendering)。DisplayList 和 TileCache 在渲染时需要按 Tile 到鼠标/焦点位置的距离排序,优先渲染用户关注区域。排序比较函数在大量 Tile 的场景中会被调用 O(N log N) 次。
加速原理
标量版本需要分别计算两个 Tile 中心到焦点的距离(各 2 次乘法 + 1 次加法 + 减法),共 10+ 次浮点运算。SIMD 版本将两个 Tile 的坐标打包进一个 128-bit 向量,一条指令同时完成:
// 将两个 Tile 的坐标打包为 [a.x, a.y, b.x, b.y]
auto res = Add(ConvertTo(df, [a.first, a.second, b.first, b.second]), Set(df, 0.5f));
// 减去焦点 [cx, cy, cx, cy],得到偏移
res = MulSub(res, Set(df, tileSize), [center.x, center.y, center.x, center.y]);
// 平方
res = Mul(res, res);
// 交换相邻对并求和:[dx²+dy², dx²+dy², dx²+dy², dx²+dy²]
res = Add(Reverse2(df, res), res);
// 比较 resVec[0] vs resVec[2]
return resVec[0] < resVec[2]; // (或 > 用于降序)
整个距离计算和比较在 5 条 SIMD 指令内完成,且完全无分支。
加速场景五:Box 降采样
BoxFilterDownsampleSIMD.cpp 是最复杂的 SIMD 加速场景,用于 ImageCodec 解码时的图像降采样。当图像解码后尺寸大于目标尺寸时,BoxFilterDownsample 使用面积平均法缩小图像。
缩放倍率分级
根据缩放比(必须为 2 的幂)选择不同的 SIMD 实现:
| 缩放比 | 实现函数 | 向量化策略 |
|---|---|---|
| ×2 | ResizeAreaFastx2SIMDFuncImpl | LoadInterleaved2 解交错相邻像素对 |
| ×4 | ResizeAreaFastx4SIMDFuncImpl | 1ch: LoadInterleaved4 四路解交错;4ch: 复用 ×16 通用路径 |
| ×8 | ResizeAreaFastx8SIMDFuncImpl | 1ch: 逐行 Load + PromoteTo 累加;4ch: 复用 ×16 通用路径 |
| ×16 | ResizeAreaFastx16SimdFuncImpl | 通用 N×N 分块累加 |
| ≥32 | ResizeAreaFastxNSimdFuncImpl | 32-bit 累加器防溢出 |
数据类型提升防溢出
小缩放比(≤16)使用 uint16_t 累加器,大缩放比(≥32)升级到 uint32_t。以 4 通道 ×16 为例:
uint8_t 像素 ──Load──→ [16 × uint8]
│
PromoteLowerTo / PromoteUpperTo
│
[8 × uint16] × 2
│
逐行累加 Add
│
LowerHalf + UpperHalf 归约
│
ShiftRightSame (除以 scale²)
│
DemoteTo ──→ [4 × uint8] 输出
位移代替除法(>> shiftNum,其中 shiftNum = 2 × log2(scale))是整数降采样的经典优化,加上 padding = scale² / 2 实现四舍五入。
非整数缩放比的加速
当缩放比不是整数时,使用加权面积平均算法。Mul 和 MulAdd 两个辅助函数也通过 Highway 加速——它们对 float 累加缓冲区进行批量标量乘法和乘加操作,用于垂直方向的加权混合。这些函数使用 ScalableTag<float>(自适应向量宽度),在 AVX2 平台上可以一次处理 8 个 float。
代码组织总览
TGFX 的所有 SIMD 加速代码集中在 5 个文件中:
| 文件 | 加速目标 | 主要调用者 |
|---|---|---|
src/core/RectSIMD.cpp | Rect::setBounds() | Path::getBounds()、路径边界计算 |
src/core/MatrixSIMD.cpp | Matrix 点变换、矩阵乘法、mapRect | 所有需要矩阵变换的绘制路径 |
src/core/VecSIMD.cpp | Vec4 全套运算 | Layer 动画系统、3D 合成 |
src/core/utils/TileSortCompareFuncSIMD.cpp | Tile 距离排序 | DisplayList、TileCache |
src/core/BoxFilterDownsampleSIMD.cpp | Box 滤波降采样 | ImageCodec 解码缩放 |
这些文件都遵循相同的 Highway 模板结构:foreach_target.h → 多 ISA 实现 → HWY_EXPORT + HWY_DYNAMIC_DISPATCH。公开 API 的调用者无需关心 SIMD 细节——Rect::setBounds()、Matrix::mapPoints() 等方法内部透明地分发到最优实现。
性能提升数据
以下是 SIMD 优化在各平台上相对标量实现的性能提升百分比(越高越好,负值表示因额外开销导致性能下降)。
mapPoints / mapRect / setBounds
| 方法 | 矩阵类型 | Web | Android | iOS | Mac | Windows | OHOS |
|---|---|---|---|---|---|---|---|
| mapPoints | translate | 53% | 76% | 69% | 24% | 83% | 36% |
| mapPoints | scale | 39% | 73% | 63% | 20% | 77% | 18% |
| mapPoints | affine | 19% | 75% | 63% | 18% | 83% | −2% |
| mapRect | translate | 22% | 68% | 60% | 28% | 66% | 69% |
| mapRect | scale | 17% | 67% | 55% | 21% | 64% | 59% |
| mapRect | affine | −129% | 33% | −14% | −90% | −110% | −94% |
| setBounds | — | 47% | 69% | 81% | 83% | 50% | 81% |
说明:
mapRect仿射变换(即 skew 不为 0)在部分平台出现负值,原因是仿射mapRect需要先将矩形展开为四角再逐点变换,SIMD 路径引入了额外的 corner 展开和setBounds归约步骤,在某些平台的标量分支预测优化较好时反而不如直接的标量实现。不过实际业务中affine场景并不常见,绝大多数矩阵变换属于 translate 或 scale 类型,因此整体收益仍然显著。
BoxFilter 降采样(Mac 平台,缩放倍率为 2 的幂)
| 缩放倍率 | 性能提升 |
|---|---|
| ×2 | 85% |
| ×4 | 74% |
| ×8 | 82% |
| ×16 | 86% |
| ×32 | 64% |
| ×64 | 62% |
说明:BoxFilter 性能数据仅在 Mac 平台测量。小缩放倍率(×2、×16)提升最为显著,因为 SIMD 向量宽度与数据块大小匹配度最高;大缩放倍率(×32、×64)由于需要升级到 32-bit 累加器且循环迭代次数增多,提升幅度相对有所回落。
与直接使用 intrinsics 的对比
| 维度 | Highway(TGFX 的方案) | 手写 intrinsics |
|---|---|---|
| 跨平台 | 一份代码自动覆盖 x86/ARM/WASM | 每个平台需要独立实现 |
| 多 ISA | 自动生成 SSE4/AVX2/AVX-512 等多版本 | 需要手动用 #ifdef 管理 |
| 运行时分发 | HWY_DYNAMIC_DISPATCH 自动探测 | 需要手写 CPUID 探测 + 函数指针 |
| 代码量 | 5 个文件约 1200 行 | 同等功能预计需要 3000-5000 行 |
| 维护成本 | Highway 版本升级即可获得新 ISA 支持 | 每个新 ISA 都需要手写适配 |
| 性能 | 接近 intrinsics(Highway 不引入额外抽象层) | 理论最优 |
TGFX 选择 Highway 的核心考量是可维护性:用接近手写性能的代价,换取了跨平台单一代码库和自动 ISA 适配能力。
