三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

SimdForEach.h

SimdForEach.h

folly/algorithm/simd/detail/SimdForEach.h 是 Folly SIMD 算法的底层遍历框架。它负责把任意未对齐区间 [f, l) 拆成:

首个部分有效的 SIMD 块

若干完整 SIMD 块

最后一个部分有效的 SIMD 块

区间外的 SIMD lane 不会单独避免读取,而是通过 ignore_extrema 标记为无效,让 delegate 在计算结果时忽略它们。

## 第 1–30 行:文件声明和依赖

- 第 1–15 行:Apache 2.0 许可证。
- 第 17 行:

#pragma once

防止头文件重复包含。

- 第 19 行:

#include <folly/CPortability.h>

提供 FOLLY_ALWAYS_INLINE 等跨平台编译器宏。

- 第 20 行:

#include <folly/Traits.h>

提供 index_constant<I>,它等价于:

std::integral_constant<std::size_t, I>

用来把循环展开位置编码进类型。

- 第 21 行:

#include <folly/algorithm/simd/Ignore.h>

提供:

ignore_none
ignore_extrema

- 第 22 行:

#include <folly/algorithm/simd/detail/UnrollUtils.h>

提供基于模板展开的 unrollUntil<N>。

- 第 23 行:

#include <folly/lang/Align.h>

提供 align_floor,用于把地址向下对齐。

- 第 25 行:

#include <array>

用于保存一次展开中的多个 SIMD 块指针。

- 第 26 行:

#include <cstdint>

提供固定宽度整数和相关底层类型。

- 第 27 行:

#include <type_traits>

提供模板元编程类型工具。

- 第 29–30 行:

namespace folly {
namespace simd::detail {

进入 Folly SIMD 内部实现命名空间。

## 第 32–38 行:设计来源和内联策略

第 32–33 行说明算法设计参考了 EVE SIMD 库的 for_each_iteration。

第 35–37 行:

// Everything is ALWAYS_INLINE because we want to have one top level noinline
// function that does everything.

这些内部模板被强制内联,目的是让最终算法集中在一个明确的顶层函数边界中,避免编译器意外留下多层 SIMD 辅助函数调用。

## 第 40–63 行:接口与 delegate 契约

### 第 41 行

simdForEachAligning<unrolling>(cardinal, f, l, delegate);

概括函数调用形式。

参数含义:

- unrolling:主循环一次展开多少个 SIMD 寄存器;
- cardinal:一个 SIMD 寄存器能容纳多少个 T;
- f、l:半开区间 [f,l);
- delegate:实际执行 SIMD 运算的对象。

例如 128 位 SSE:

T = uint8_t → cardinal = 16
T = uint16_t → cardinal = 8
T = uint32_t → cardinal = 4
T = uint64_t → cardinal = 2

### 第 43–47 行:为什么可以向前读取

算法可能把 f 向下对齐到 f 之前的地址,然后加载整个 SIMD 寄存器。

假设:

cardinal = 4
f 指向索引 2

那么实际加载可能从索引 0 开始:

加载:[0, 1, 2, 3]
忽略:[0, 1]
有效: [2, 3]

作者依据的硬件事实是:内存页通常按 4 KiB 对齐。如果原始地址有效,把它向下对齐到一个较小 SIMD 边界,通常仍位于同一页,因此硬件读取不会跨入未映射页面。

不过,这属于底层 SIMD 技巧:

- 必须在结果中屏蔽数组外 lane;
- 相关读取需要关闭或特殊处理 ASan;
- 它依赖 Folly 的底层平台加载封装,不应作为普通 C++ 指针访问模式模仿。

### 第 49–53 行

说明四个接口参数。

### 第 54–62 行:delegate 必须提供的操作

delegate 概念上是回调对象,但需要支持两个成员函数。

第一个:

bool step(T*, ignore, unrollIndex);

代码中的实际调用还包含第三个“展开位置”参数。

它处理一个 SIMD 寄存器:

- 首尾块传入 ignore_extrema;
- 完整块传入 ignore_none;
- 返回 true 表示要求提前终止。

第二个:

bool unrolledStep(std::array<T*, unrolling>);

它一次处理 unrolling 个完整 SIMD 块。

当 unrolling == 1 时不会调用这个接口。

delegate 通过引用传入,所以它可以保存结果和状态。例如 simdAnyOf 的 delegate 会保存“是否已有任何 lane 匹配”。

## 第 64–66 行:提前声明

template <int unrolling, typename T, typename Delegate>
FOLLY_ALWAYS_INLINE void simdForEachAligning(
int cardinal, T* f, T* l, Delegate& delegate);

声明主入口,定义位于第 176–208 行。

模板参数:

- unrolling:编译期展开因子;
- T:元素类型,可由指针推导;
- Delegate:处理对象类型,可由参数推导。

返回 void。处理结果由 delegate 自己保存。

## 第 68–77 行:计算前一个对齐地址

### 第 74–75 行

template <typename T>
FOLLY_ALWAYS_INLINE T* previousAlignedAddress(T* ptr, int to) {

定义辅助函数,将 ptr 向下对齐。

to 的单位是“元素个数”,而非字节。

### 第 76 行

return align_floor(ptr, sizeof(T) * to);

align_floor 接受的是字节对齐值,所以换算为:

sizeof(T) × cardinal

例如:

T = uint32_t
cardinal = 4
对齐字节数 = 4 × 4 = 16

如果 ptr 地址为 0x1014,向下按 16 字节对齐后得到 0x1010。

align_floor 内部通过类似操作实现:

address & ~(alignment - 1)

因此传入的字节对齐量必须是非零的 2 的幂。

### 第 77 行

结束 previousAlignedAddress。

## 第 79–93 行:主循环类说明

SimdForEachMainLoop 只负责处理首尾部分块之间的完整 SIMD 块。

它有两个版本:

- 展开因子为 1;
- 展开因子大于 1。

其 operator() 返回:

- true:delegate 要求提前结束;
- false:正常处理到 l。

## 第 93–105 行:不展开版本

### 第 93 行

struct SimdForEachMainLoop {

用函数对象承载两个 operator() 重载。

### 第 94–96 行

template <typename T, typename Delegate>
FOLLY_ALWAYS_INLINE bool operator()(
int cardinal, T*& f, T* l, Delegate& delegate, index_constant<1>) const {

这是 unrolling == 1 的专门版本。

注意 f 是 T*&,即“指针的引用”。函数推进 f 后,调用者看到的 af 也会同步改变。

最后一个参数 index_constant<1> 用于在编译期选择此重载。

### 第 97 行

while (f != l) {

遍历所有完整 SIMD 块。

这里要求 f、l 均已对齐,且距离是 cardinal 的整数倍。

### 第 98 行

if (delegate.step(f, ignore_none{}, index_constant<0>{})) {

处理从 f 开始的一个完整寄存器:

- ignore_none{}:全部 lane 有效;
- index_constant<0>{}:展开位置为 0,因为没有展开。

### 第 99 行

return true;

delegate 请求终止,向上传递提前退出信号。

### 第 101 行

f += cardinal;

把指针移动一个 SIMD 寄存器的元素数量。

### 第 104 行

return false;

完整处理到 l,没有提前终止。

## 第 107–125 行:单步展开辅助对象

这个对象用于 unrolling > 1 时连续执行最多 unrolling 次普通 step。

### 第 107–108 行

template <typename T, typename Delegate>
struct SmallStepsLambda {

它是传给 UnrollUtils::unrollUntil 的函数对象。

### 第 109 行

bool& shouldBreak;

引用外部布尔值,用来保存 delegate 是否请求终止。

### 第 110 行

int cardinal;

一个 SIMD 寄存器包含的元素数。

### 第 111 行

T*& f;

当前位置指针的引用。每处理一个寄存器都会推进外部指针。

### 第 112 行

T* l;

完整块区间的尾后指针。

### 第 113 行

Delegate& delegate;

实际处理 SIMD 数据的对象。

### 第 115–116 行

template <std::size_t i>
FOLLY_ALWAYS_INLINE bool operator()(index_constant<i> unrollI) {

每次模板展开调用一次。

i 是编译期常量:

0, 1, 2, ..., unrolling - 1

### 第 117–119 行

if (f == l) {
return true;
}

完整块已经处理完毕,要求 unrollUntil 停止继续展开。

这里返回 true 只表示“停止模板展开”,不代表 delegate 请求提前退出。因此并不会设置 shouldBreak。

### 第 121 行

shouldBreak = delegate.step(f, ignore_none{}, unrollI);

处理当前完整 SIMD 块,并把当前展开编号传给 delegate。

delegate 可以利用 unrollI 区分多组独立累加器,降低依赖链。

### 第 122 行

f += cardinal;

移动到下一个 SIMD 块。

即使 delegate 返回 true,这里仍会推进一次指针;由于随后整个遍历会退出,这个差异不影响算法结果。

### 第 123 行

return shouldBreak;

如果 delegate 请求终止,利用 unrollUntil 的短路行为停止后续展开调用。

### 第 125 行

结束辅助对象。

## 第 127–173 行:展开版本主循环

### 第 127–130 行

template <typename T, typename Delegate, std::size_t unrolling>
FOLLY_ALWAYS_INLINE bool operator()(
int cardinal, T*& f, T* l, Delegate& delegate, index_constant<unrolling>)
const {

这是一般展开版本。

最后一个参数把展开因子编码进类型。调用:

index_constant<4>{}

就会实例化 unrolling == 4 的版本。

index_constant<1> 会优先匹配前面的专门重载。

### 第 131–142 行:为什么先做单步

作者比较了三种方法:

1. Duff’s device;
2. 先运行展开循环,再处理剩余单步;
3. 先做一组普通单步,再运行展开循环。

这里选择第 3 种。

原因是 unrolledStep 往往需要准备多个寄存器、多个中间值。如果数组很短,先执行普通 step 可以在到达末尾后直接返回,完全避免初始化展开处理逻辑。

### 第 144–146 行

while (true) {

外层循环通常执行一次,最多因为末尾剩余的零散完整块再执行一次。

例如共有 11 个完整块、展开因子为 4:

先单步处理 4 个
展开处理 4 个
剩余 3 个
回到 while,再单步处理 3 个

### 第 147–149 行

bool shouldBreak = false;

记录单步阶段停止的原因:

- true:delegate 请求终止;
- false:只是到达 l。

### 第 151–153 行

if (UnrollUtils::unrollUntil<unrolling>(SmallStepsLambda<T, Delegate>{
shouldBreak, cardinal, f, l, delegate})) {

构造 SmallStepsLambda,然后将其在编译期展开 unrolling 次。

对于 unrolling == 4,概念上相当于:

op(index_constant<0>{}) ||
op(index_constant<1>{}) ||
op(index_constant<2>{}) ||
op(index_constant<3>{});

由于使用逻辑或的短路规则,一次调用返回 true 后,后续调用不会执行。

### 第 154 行

return shouldBreak;

如果单步阶段停止:

- 因 f == l 停止时,shouldBreak == false;
- 因 delegate 返回 true 停止时,shouldBreak == true。

因此可以准确向调用者区分“正常结束”和“提前结束”。

### 第 157 行

for (std::ptrdiff_t bigStepsCount = (l - f) / (cardinal * unrolling);

计算剩余区间包含多少个完整的“展开组”。

一个展开组处理:

cardinal × unrolling

个元素。

例如:

cardinal = 8
unrolling = 4

每个展开组处理 32 个元素。

使用 std::ptrdiff_t,因为两个指针相减的结果类型就是 ptrdiff_t。

### 第 158–159 行

bigStepsCount != 0;
--bigStepsCount

每轮消耗一个完整展开组,直到没有完整组为止。

### 第 160 行

std::array<T*, unrolling> arr;

创建指针数组,保存这一展开组中每个 SIMD 块的起始地址。

例如 cardinal == 4、unrolling == 3:

arr[0] = f
arr[1] = f + 4
arr[2] = f + 8

### 第 161 行

注释说明填充数组的 lambda 完全可内联,所以不需要额外回调结构。

### 第 162–166 行

UnrollUtils::unrollUntil<unrolling>([&](auto idx) {
arr[idx()] = f;
f += cardinal;
return false;
});

模板展开地填充 arr。

idx 是 index_constant<I> 对象,调用 idx() 得到编译期值 I。

lambda 永远返回 false,所以一定执行全部 unrolling 次。

对于展开因子 4,逻辑等价于:

arr[0] = f; f += cardinal;
arr[1] = f; f += cardinal;
arr[2] = f; f += cardinal;
arr[3] = f; f += cardinal;

### 第 167 行

if (delegate.unrolledStep(arr)) {

把整组指针交给 delegate。

delegate 可以:

1. 连续加载多个寄存器;
2. 对每个寄存器执行 SIMD 谓词;
3. 合并多个寄存器的逻辑结果;
4. 最后只执行一次横向归约。

例如 simdAnyOf 会先分别比较,再用 SIMD logical_or 合并,最后调用一次 Platform::any。

### 第 168 行

return true;

delegate 请求提前终止。

### 第 170 行

结束展开组循环。

### 第 171 行

结束本轮 while (true)。

如果展开组之后还剩少于 unrolling 个完整 SIMD 块,重新进入循环,由开头的 SmallStepsLambda 处理它们。

### 第 172–173 行

结束展开版本和 SimdForEachMainLoop。

## 第 175–208 行:主遍历入口

### 第 176–178 行

template <int unrolling, typename T, typename Delegate>
FOLLY_ALWAYS_INLINE void simdForEachAligning(
int cardinal, T* f, T* l, Delegate& delegate) {

定义主函数。

隐含前置条件包括:

- [f,l) 是合法半开区间;
- f <= l;
- cardinal > 0;
- sizeof(T) * cardinal 是合法的 2 的幂对齐值;
- unrolling >= 1。

### 第 179–181 行

if (f == l) {
return;
}

空区间直接返回。

这也避免后续对空区间地址执行对齐、加载和指针距离计算。

### 第 183 行

T* af = previousAlignedAddress(f, cardinal);

把起点 f 向下对齐到 SIMD 寄存器边界。

af 表示 aligned first。

例如:

cardinal = 4
f = base + 6
af = base + 4

### 第 184 行

T* al = previousAlignedAddress(l, cardinal);

把尾后指针 l 也向下对齐。

al 表示 aligned last。

注意它不是向上取整。例如:

l = base + 15
al = base + 12

从 al 开始的寄存器就是最后一个可能部分有效的 SIMD 块。

### 第 186 行

ignore_extrema ignore{static_cast<int>(f - af), 0};

计算首块需要忽略多少个前导 lane。

例如:

af = base + 4
f = base + 6

那么:

ignore.first = 2;
ignore.last = 0;

首块布局为:

索引: 4 5 6 7
状态: 忽略 忽略 有效 有效

### 第 187 行

if (af != al) {

判断起点和终点是否位于不同的对齐块。

- af == al:整个区间位于同一个 SIMD 块中;
- af != al:存在独立首块,之后还可能有完整块和尾块。

### 第 188–191 行:处理首块

if (delegate.step(af, ignore, index_constant<0>{})) {
return;
}

从向下对齐的 af 加载一个完整 SIMD 寄存器,但通过 ignore.first 忽略 f 之前的 lane。

首块使用展开编号 0。

如果 delegate 已找到结果,例如 any_of 找到匹配值,就立即返回。

### 第 192 行

ignore.first = 0;

首块已处理完。后续最终尾块不会有前导无效元素,因此清除 first。

如果 af == al,代码不会进入此分支,所以同一块同时作为首尾块时,原来的 ignore.first 会保留下来。

### 第 193 行

af += cardinal;

移动到首块之后的下一个对齐块。

从这里开始,af 指向完整 SIMD 块区间的起点。

### 第 195–196 行

if (SimdForEachMainLoop{}(
cardinal, af, al, delegate, index_constant<unrolling>{})) {

处理 [af,al) 中的所有完整 SIMD 块。

af 按引用传入,因此正常完成后:

af == al

最后一个参数在编译期选择:

- unrolling == 1 的简单循环;
- 一般展开循环。

### 第 197–198 行

return;

中间处理阶段如果 delegate 请求提前终止,整个遍历立即结束。

### 第 200 行

// Here af might be exactly at the end of page.

处理完中间块后,af 可能正好等于内存页末端或区间尾端,不能无条件再加载一个寄存器。

### 第 201–203 行

if (af == l) {
return;
}

如果 l 本身已经对齐,那么没有尾部部分块。

此时必须直接返回,不能再执行:

delegate.step(af, ...)

否则会从尾后地址额外读取一个 SIMD 寄存器,甚至可能跨入未映射页面。

### 第 204 行

结束“首尾位于不同对齐块”的分支。

### 第 206 行

ignore.last = static_cast<int>(af + cardinal - l);

计算最后一个 SIMD 块末端超出 l 的 lane 数量。

例如:

af = base + 12
cardinal = 4
l = base + 15

则:

ignore.last = 12 + 4 - 15 = 1;

尾块布局为:

索引: 12 13 14 15
状态: 有效 有效 有效 忽略

如果整个区间位于同一块中,此时 ignore.first 和 ignore.last 会同时非零。

例如:

cardinal = 4
f = base + 1
l = base + 3

得到:

ignore.first = 1;
ignore.last = 1;

只有中间两个 lane 有效。

### 第 207 行

delegate.step(af, ignore, index_constant<0>{});

处理最后一个部分有效的 SIMD 块。

这里没有检查返回值,因为它已经是最后一次调用;无论 delegate 返回什么,函数都会立即结束。

### 第 208 行

结束 simdForEachAligning。

### 第 210–211 行

关闭 folly::simd::detail 和 folly 命名空间。

## 一个完整例子

假设:

cardinal = 4
f = base + 1
l = base + 19

即有效区间是 [1,19)。

地址被拆分为:

块 [0,4) :忽略第 0 个 lane,处理 1、2、3
块 [4,8) :完整
块 [8,12) :完整
块 [12,16) :完整
块 [16,20) :处理 16、17、18,忽略第 19 个 lane

对应执行顺序:

delegate.step(base + 0, {first=1,last=0}, index 0)
SimdForEachMainLoop 处理 [base+4, base+16)
delegate.step(base + 16, {first=0,last=1}, index 0)

核心思想可以压缩为:

af = floor_align(f);
al = floor_align(l);

处理首块,屏蔽 f 之前的 lane;
处理 [首块之后, al) 的完整块;
处理尾块,屏蔽 l 之后的 lane;

这让上层 SIMD 算法只需要实现“如何处理一个或多个寄存器”,不用反复处理地址未对齐、短数组、尾部残余和循环展开等边界问题。

← 返回列表