Skip to content

Commit f62dde3

Browse files
committed
More Speedups for Tuple Deformation: Precalculating attcacheoff
1 parent 6b85900 commit f62dde3

7 files changed

Lines changed: 332 additions & 0 deletions

File tree

src/SUMMARY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# 🇬🇧 English
44

55
- [2026](./en/2026/README.md)
6+
- [Week 09](./en/2026/09/README.md)
7+
- [More Speedups for Tuple Deformation: Precalculating attcacheoff](./en/2026/09/tuple-deformation-speedups.md)
68
- [Week 08](./en/2026/08/README.md)
79
- [Eliminating SPI from RI Triggers: A Fast Path for Foreign Key Checks](./en/2026/08/ri-fast-path-foreign-key-checks.md)
810
- [Week 07](./en/2026/07/README.md)
@@ -24,6 +26,8 @@
2426
# 🇨🇳 中文
2527

2628
- [2026](./cn/2026/README.md)
29+
- [第 09 周](./cn/2026/09/README.md)
30+
- [元组解构的进一步加速:预计算 attcacheoff](./cn/2026/09/tuple-deformation-speedups.md)
2731
- [第 08 周](./cn/2026/08/README.md)
2832
- [消除 RI 触发器中的 SPI:外键检查的快速路径](./cn/2026/08/ri-fast-path-foreign-key-checks.md)
2933
- [第 07 周](./cn/2026/07/README.md)

src/cn/2026/09/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# 第 09 周(2026)
2+
3+
2026 年第 09 周的 PostgreSQL 邮件列表讨论。
4+
5+
🇬🇧 [English Version](../../../en/2026/09/index.html)
6+
7+
## 文章
8+
9+
- [元组解构的进一步加速:预计算 attcacheoff](./tuple-deformation-speedups.md)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# 元组解构的进一步加速:预计算 attcacheoff
2+
3+
## 引言
4+
5+
**元组解构(tuple deformation)** 是将 PostgreSQL heap 元组的原始字节表示解包为 `TupleTableSlot` 中各个属性值的过程。它在查询执行中无时无刻不发生——每次顺序扫描、索引扫描或连接产生一行时,执行器都必须对元组进行解构才能访问列值。对于处理数百万行的负载,即便对解构热路径做小幅优化,也能带来可观的性能提升。
6+
7+
David Rowley 一直在持续优化元组解构。在 PostgreSQL 18 中,他已合并多项补丁:`CompactAttribute`(5983a4cff)、更快的偏移对齐(db448ce5a)以及内联解构循环(58a359e58)。在此基础上,他提出**预计算 `attcacheoff`**,而不是在每次属性访问时计算。讨论已演进至 v10(2026 年 2 月),**Andres Freund** 贡献了 NULL 位图转 isnull 的方案,使 Apple M2 在部分场景下加速达 **63%**。补丁集仍在积极审查中。
8+
9+
## 为什么重要
10+
11+
当执行器需要从元组中读取某列值时,必须:
12+
13+
1. **对齐**:按属性对齐要求对齐当前偏移
14+
2. **获取**:通过 `fetch_att()` 读取值
15+
3. **前移**:跳过当前属性到下一个
16+
17+
这些步骤形成依赖链:每个偏移都依赖前一个。指令级并行空间有限。对于定长属性,PostgreSQL 可以缓存偏移(`attcacheoff`)以避免重复计算对齐和长度——但此前缓存是在解构循环*内部*完成的。David 的想法是:在 `TupleDesc` 初始化完成时**一次性**计算,而不是对每个元组都算一遍。
18+
19+
## 技术方案
20+
21+
### TupleDescFinalize()
22+
23+
核心改动是引入 `TupleDescFinalize()`,必须在 `TupleDesc` 创建或修改后调用。该函数会:
24+
25+
1. **预计算**所有定长属性的 `attcacheoff`
26+
2. **记录** `firstNonCachedOffAttr`——第一个无法缓存偏移的属性的 `attnum`(即首个 varlena 或 cstring 属性)
27+
3. **启用**一个紧凑循环,先处理所有有缓存偏移的属性,再进入需要手动计算偏移的属性
28+
29+
如果元组在最后一个有缓存偏移的属性之前存在 NULL,则只能使用 `attcacheoff` 到该 NULL 为止——但对于没有早期 NULL 的元组,快速路径可以在一个紧凑循环中处理大量属性,而无需任何按属性的偏移运算。
30+
31+
### 专用解构循环
32+
33+
补丁添加了一个专用循环,先处理所有有预计算 `attcacheoff` 的属性,再进入处理 varlena/cstring 属性的循环。对于设置了 `HEAP_HASNULL` 的元组,当前代码会对每个属性调用 `att_isnull()`。进一步优化是:在遇到第一个 NULL 之前,持续解构而不调用 `att_isnull()`。基准测试中的场景 #5(首列 int not null、末列 int null)最能体现这一点——常表现为最大加速。
34+
35+
### 可选的 OPTIMIZE_BYVAL 循环
36+
37+
可选变体针对所有被解构属性均为 `attbyval == true` 的元组增加一个循环。此时可以内联 `fetch_att()`,而无需处理指针类型的分支,从而减少分支、获得更紧凑的循环。代价是:当该优化不适用时,需要额外检查 `attnum < firstByRefAttr`。基准测试中,不同硬件和编译器对是否启用该优化效果不一。
38+
39+
## 基准测试设计
40+
41+
为最大化解构工作占总 CPU 的比例,David 设计了如下基准查询:
42+
43+
```sql
44+
SELECT sum(a) FROM t1;
45+
```
46+
47+
其中 `a` 列几乎在最后,因此必须先解构前面的所有属性才能读取 `a`。八种表结构涵盖首列(int/text、null/not null)和末列(int null/not null)的组合。对每种表结构,分别在 0、10、20、30、40 个额外 `INT NOT NULL` 列下运行——每次基准运行包含 40 个场景,每个场景 100 万行。
48+
49+
## 基准测试结果
50+
51+
结果因硬件和编译器而异:
52+
53+
- **AMD Zen 2(3990x)+ GCC**:启用 `OPTIMIZE_BYVAL` 时平均加速达 **21%**;部分测试超过 **44%**;无回退。
54+
- **AMD Zen 2 + Clang**:0 额外列场景下存在小幅回退。
55+
- **Apple M2**:场景 #1#5 提升明显;其余提升较小;部分补丁有轻微回退。
56+
- **Intel(Azure)**:在共享、少核实例上运行,因与其他负载共享 L3,结果噪声较大。
57+
58+
## 补丁演进
59+
60+
### v1 → v3(2025 年 12 月 – 2026 年 1 月)
61+
62+
- **v1**:三个补丁——0001(预计算 attcacheoff)、0002(实验性 NULL 位图前瞻)、0003(移除专用 hasnulls 循环)
63+
- **v2**:代码库同步、修复 0003 中 NULL 位图 Assert、JIT 修复(移除 `TTS_FLAG_SLOW`)、更多基准
64+
- **v3**:代码库同步、放弃 0002 和 0003(基准收益有限)、仅保留 0001
65+
66+
### v4(2026 年 1 月)
67+
68+
回应 Chao Li 的代码审查:
69+
70+
- **NULL 位图 mask**(tupmacs.h):补充注释——当 `natts & 7 == 0` 时 mask 为 0,代码会正确返回 `natts`
71+
- **未初始化 TupleDesc**`firstNonCachedOffAttr == 0` 表示无缓存属性;`-1` 表示未初始化。添加 Assert,失败时提示调用 `TupleDescFinalize()`
72+
- **拼写**:"possibily" → "possibly"
73+
- **LLVM**:修复编译警告
74+
75+
### v5–v8(2026 年 1–2 月):Andres Freund 的 NULL 位图优化
76+
77+
**Andres Freund** 加入讨论并提出关键改进:不再对每列调用 `att_isnull()`,而是用 SWAR(SIMD Within A Register)技术直接从 NULL 位图计算 `isnull[]` 数组。思路是:将位图的一个字节乘以精心选定的值(如 `0x204081`),使每位扩散到独立字节,再掩码。这样无需 2KB 查找表,在多数硬件上效果良好。
78+
79+
David 在补丁 0004(“Various experimental changes”)中实现了该方案。0004 的其他改动包括:
80+
81+
- **`populate_isnull_array()`**:用乘法技巧批量将 NULL 位图转换为 `tts_isnull`
82+
- **`tts_isnull` 大小**:向上取整到 8 的倍数,使循环可一次写 8 字节(避免 `memset` 内联问题)
83+
- **`t_hoff`**:对 `!hasnulls` 元组,使用 `MAXALIGN(offsetof(HeapTupleHeaderData, t_bits))` 替代 `t_hoff`
84+
- **`fetch_att_noerr()`**:新增无 `elog` 的变体,用于常见的 `attlen == 8` 情况
85+
86+
**John Naylor** 指出当字节为 255 时 `__builtin_ctz(~bits[bytenum])` 未定义;David 通过强制转换修复:`pg_rightmost_one_pos32(~((uint32) bits[bytenum]))`
87+
88+
启用 0004 后,Apple M2 平均比 master 快 **53%**(排除 0 额外列测试约 **63%**)。Andres 建议使用 `pg_nounroll``pg_novector` pragma 防止 GCC 对 `populate_isnull_array()` 过度向量化,该函数曾生成低效代码。
89+
90+
### v9(2026 年 2 月 24 日)
91+
92+
- **补丁重排**`deform_bench` 移至 0001,便于在 master 上做基准测试
93+
- **0004(新)**`slot_getsomeattrs` 的 sibling-call 优化——将 `slot_getmissingattrs()` 移入 `getsomeattrs()`,使编译器可应用尾调用优化,降低开销并改善 0 额外列测试
94+
- **0005(新)**:将 `CompactAttribute` 从 16 字节缩小到 8 字节——`attcacheoff` 改为 `int16`(最大 2^15),布尔用位标志。Andres 指出 8 字节便于编译器使用 scale factor 8 的单条 LEA;6 字节则需两条 LEA
95+
96+
### v10(2026 年 2 月 25 日)— 最新补丁集
97+
98+
基于实际 v10 补丁内容:
99+
100+
**0003(优化元组解构)**
101+
- `firstNonCachedOffsetAttr`:首个无缓存偏移的属性的索引
102+
- `firstNonGuaranteedAttr`:首个可为 NULL、缺失或 `!attbyval` 的属性的索引。仅解构到此属性时,无需访问 `HeapTupleHeaderGetNatts(tup)`,减少 CPU 流水线依赖
103+
- `TTS_FLAG_OBEYS_NOT_NULL_CONSTRAINTS`:保证属性优化的可选标志(部分代码在 NOT NULL 校验前即解构元组)
104+
- `populate_isnull_array()`:使用 `SPREAD_BITS_MULTIPLIER_32`(0x204081)将反转的 NULL 位图每位扩散到独立字节;分低 4 位和高 4 位处理以避免 uint64 溢出
105+
- `fetch_att_noerr()`:无 `elog``fetch_att()` 变体;当 attlen 来自 `CompactAttribute` 时安全
106+
- `first_null_attr()`:用 `pg_rightmost_one_pos32``__builtin_ctz` 查找位图中首个 NULL
107+
108+
**0004(sibling-call 优化)**
109+
- `getsomeattrs()` 现负责调用 `slot_getmissingattrs()`
110+
- `slot_getmissingattrs()`:用 for 循环替代 `memset`(基准显示循环更快)
111+
- `slot_deform_heap_tuple()`:在 `attnum < reqnatts` 时于末尾调用 `slot_getmissingattrs()`;参数由 `natts` 改为 `reqnatts`
112+
113+
**0005(8 字节 CompactAttribute)**
114+
- `attcacheoff` 改为 `int16`;大于 `PG_INT16_MAX` 的偏移不缓存
115+
- `attispackable``atthasmissing``attisdropped``attgenerated` 使用位标志
116+
- 保存 `cattrs = tupleDesc->compact_attrs` 以帮助 GCC 生成更优代码(避免重复 `TupleDescCompactAttr()` 调用)
117+
118+
**审查修复**
119+
- **Amit Langote**:修复 rebase 噪声(重复的 `attcacheoff` 检查)
120+
- **Zsolt Parragi**:大端序修复——在 `populate_isnull_array()``memcpy` 前加入 `pg_bswap64()`
121+
- **拼写**:"benchmaring" → "benchmarking","to info" → "into"
122+
- **Andres**:在 `slot_getmissingattrs` 前设置 `*offp` 以减少栈溢出;将 `attnum` 改为 `size_t` 以修复 GCC `-fwrapv` 下的代码生成
123+
124+
### deform_bench 与基准基础设施
125+
126+
**Andres****Álvaro Herrera** 讨论了 `deform_bench` 的放置:`src/test/modules/benchmark_tools``src/benchmark/tuple_deform`,或单一微基准扩展。Andres 主张逐步合并有用工具,而非等待完整套件。David 倾向于先完成解构优化补丁;deform_bench 可能单独提交。
127+
128+
## 代码审查:Chao Li 的反馈
129+
130+
Chao Li 审查了补丁并提出几点:
131+
132+
1. **NULL 位图 mask**:补充注释说明 `natts & 7 == 0` 时无溢出/OOB 风险
133+
2. **未初始化 TupleDesc**:在 TupleDesc 创建时将 `firstNonCachedOffAttr` 初始化为 `-1`;在 `nocachegetattr()` 中断言 `>= 0`
134+
3. **语义一致性**:用 0 表示“无缓存属性”,>0 表示“有缓存”
135+
4. **拼写**:"possibily" → "possibly"
136+
137+
David 在 v4 中均已处理。
138+
139+
## 当前状态
140+
141+
- **v10**(2026 年 2 月)为最新补丁集:0001(deform_bench)、0002(TupleDescFinalize 桩)、0003(主优化)、0004(sibling-call + NULL 位图→isnull)、0005(8 字节 CompactAttribute)
142+
- **Andres Freund** 支持合并 0004,认为收益明显;0005 的收益较不确定(在解构少量列时有助于 LEA 寻址)
143+
- **Zsolt Parragi**(Percona)、**Álvaro Herrera****John Naylor****Amit Langote** 持续参与审查
144+
- deform_bench 的放置(src/test/modules 或 src/benchmark)仍在讨论;David 希望先落地优化补丁
145+
146+
## 结论
147+
148+
`TupleDescFinalize()` 中预计算 `attcacheoff`,并为有缓存偏移的属性使用专用紧凑循环,可为现代 CPU 上的元组解构带来可观的加速。当元组具有大量定长列且 NULL 较少或较晚出现时,优化效果最佳。结合 Andres Freund 的 NULL 位图转 isnull 方案(“0x204081” SWAR 技巧),Apple M2 在排除边缘情况后可达 **63%** 加速。`slot_getsomeattrs` 的 sibling-call 优化进一步降低开销。结果因硬件和编译器而异;GCC 可能对部分循环过度向量化,可通过 pragma 或将循环索引改为 `size_t` 缓解。补丁集(v10)经 Andres、John Naylor、Zsolt Parragi、Álvaro Herrera、Amit Langote 等多轮审查,正在向集成推进。
149+
150+
## 参考资料
151+
152+
- [讨论:More speedups for tuple deformation](https://www.postgresql.org/message-id/flat/CAApHDvpoFjaj3%2Bw_jD5uPnGazaw41A71tVJokLDJg2zfcigpMQ%40mail.gmail.com)
153+
- 相关 v18 工作:5983a4cff(CompactAttribute)、db448ce5a(更快偏移对齐)、58a359e58(内联解构循环)

src/cn/2026/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
## 各周
66

7+
- [第 09 周](./09/index.html)
8+
- [元组解构的进一步加速:预计算 attcacheoff](./09/tuple-deformation-speedups.md)
79
- [第 08 周](./08/index.html)
810
- [消除 RI 触发器中的 SPI:外键检查的快速路径](./08/ri-fast-path-foreign-key-checks.md)
911
- [第 07 周](./07/index.html)

src/en/2026/09/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Week 09 (2026)
2+
3+
PostgreSQL mailing list discussions for Week 09, 2026.
4+
5+
🇨🇳 [中文版本](../../../cn/2026/09/index.html)
6+
7+
## Articles
8+
9+
- [More Speedups for Tuple Deformation: Precalculating attcacheoff](./tuple-deformation-speedups.md)

0 commit comments

Comments
 (0)