PJ
lab1
V1 swiss_table
design philosophy:
- 我干不过现有的库
跑起来被同学代码薄纱,且有抄袭原有库的可能。
自己不怎么看得懂代码,codex写出来的代码有很多冗余,优化空间比较小。
V2 朴素手写开放寻址
design philosophy:
- 我要掌握自己的代码,我要手写
- 开放寻址,优化空间主要在于哈希函数
problem:指令集用处不大(?)
V2_1
design philosophy:
- 没有其他方法,尝试优化空间
- 如果空间开到不是 2 的幂次方呢?
如果一组在 $[0,2^{64})$ 的哈希值被认为是均匀分布的,尝试 $(n*key)>>64$,这样也可以认为是均匀分布的。
problem:还是干不过他们啊。
V3
来源于赛后助教下发的PPT。
design philosophy:
- 开放地址法+SIMD指令集进行查找
- 节省内存,不使用控制字节,采用所有数据集没有的一个uint64来作为空槽标志(这个做法在 lab2 被 ban 了)
- 冲突比较少,哈希函数的效率很重要。哈希函数使用内置的 _mm_crc32_u64。
- 申请内存会进行内存对齐,使用 huge page
- 对分支预测添加hints来减少分支预测错误导致的bubble。不过我觉得用处不大。
lab2
我们需要关注 PJ 条件的特殊性,这是我们能优于现有的库的关键:
- 只需要实现动态插入,查询,区间查询,不需要实现删除和更新
- 仅支持单线程
- key 和 value 都是 uint64_t 类型,key 互不相同
- bulk load 不算时间
调研了各种学习性索引,考虑到PJ要求是动态插入,区间查询,单线程,所以排除了一些:
ART:最稳妥的传统基线,随机数据和从 0 在线长起来都很稳、点查和插入都强,但区间查找通常不如叶链式结构直接,综合上是“最不容易翻车”的方案。
B+Tree:最经典、最均衡、最容易保证正确性的方案,区间查找天然友好、尾延迟稳定,但点查和插入常数通常不如优秀的内存型结构或 learned index 激进。
ALEX:对这个题目最对口的 learned index 之一,内部节点模型路由 + 叶子 gap array + 叶链扫描,使它在单线程 mixed workload 且带 range 的场景里很强,但分布不友好时稳定性和 range 局部性会变差。
HIRE:如果按“你的题目背景”来选,它是最贴题的新一代 mixed-workload learned index,因为它专门强化了 range、稳定性和尾延迟,但实现复杂度明显高于 ALEX,完整版还强依赖后台重训练机制。需要多线程。
DILI:适合做为 ALEX 之后最值得补的 learned index,支持动态插入并试图减少叶内最后一步搜索,对点查和插入很有潜力,但论文对 range 的系统性证据不如 ALEX/HIRE 充分。
LIPP:点查和插入都可能很猛,因为它追求精确位置、几乎不做叶内纠错,但它为了这个牺牲了数据局部性和叶链式连续扫描,所以对你这种带区间查找的题目通常不合适。不能区间查询。
PGM / DPGM:更适合读多或纯写更重的模式,尤其 DPGM 对持续插入很友好,但它们的缓冲/多层合并会让 range 和 mixed workload 的综合表现吃亏,不是你这个题的主选。慢。
MAB+tree:属于能做、也能支持 range 的中间路线,整体思路比 ALEX 更保守,但在你这个随机数据 + mixed workload 的设定下,通常没有 ALEX 那么值得优先投入。慢。
LiBox:如果硬件有 AVX-512,它在 mixed workload 下可能很强,但它对指令集条件比较挑,而且论文自己也承认 range 上没有特别稳定的明显优势,所以更像“高风险高收益的后手”。区间查询慢。
学习性索引:ALEX ,HYPER
传统索引:B+Tree
学习性索引
V1
design philosophy:
- codex 很牛
- Alex 很牛
带着 codex 读了一下论文,让他写点东西,效果略逊 B+Tree。
problem:
- codex 没有那么牛
- Alex 的库函数实现包括了很多细节,比如计算需要 expand 还是 split 的 cost 函数,fanout 取多少…
V1_2
design philosophy:
- codex 不牛,我需要看懂思路,自己写一个出来
- 阅读 alex 核心库
尝试阅读原库,过于工程,且总代码有 6k 多行,烧 token 巨快。放弃实现。
V2
design philosophy:
- 尝试实现 HYPER?
或许性价比不高,已经不想卷了。
传统索引 - B+Tree
V3
design philosophy:
- 参考 PJ1,需要自己掌握代码,我要看懂代码,最好能自己写出来
- 分析库函数的 B+ 树效率欠佳的原因,考虑优化方向:
- 完全不考虑 delete 操作,减少代码复杂度并优化内存
- 牺牲一点空间,进行节点内存预分配
- 针对内存排布优化(leaf/internal 分离优化内存,key/value 分离优化SIMD)
- 分叉数
- 对于查找时,可以分析线性查找/SIMD(AVX2/AVX512)/二分/SIMD结合别的效果
- 64B 对齐,优化 cache miss
- huge page 优化 TLP miss
memmove处理插入移动- CSB+Tree 风格 child 连续分配(待求证),child group 连续分配等
V3_1
problem:
keys[]没有真正 64B 对齐 -> 舍弃一点点内存- bulkload 叶子填满效率不高,可以考虑变成 70 到 90 填充
- internal _max 可以和 leaf_max 不一样,例如 32/64, 64/96/128 的组合
- 内存池管理写的非常丑陋-> 尝试去掉 mmap
- parent 可以用 path
- 指针可以用内存池下标优化
接下来先尝试加 AVX2 ,去掉 mmap-> 去掉 mmap 和 huge page之后,效率下降。
AVX2 效果比 AVX512 好的多。
V4
read_cnt=0, insert_cnt=0, update_cnt=0, scan_cnt=999257, lower_bound_cnt=99000743, delete_cnt=0
read_cnt=0, insert_cnt=0, update_cnt=0, scan_cnt=20000000, lower_bound_cnt=0, delete_cnt=0
read_cnt=23748912, insert_cnt=99999060, update_cnt=0, scan_cnt=1252028, lower_bound_cnt=0, delete_cnt=0
read_cnt=95004975, insert_cnt=99997714, update_cnt=0, scan_cnt=4997311, lower_bound_cnt=0, delete_cnt=0
read_cnt=379947315, insert_cnt=100000000, update_cnt=0, scan_cnt=19991264, lower_bound_cnt=0, delete_cnt=0
read_cnt=0, insert_cnt=98999063, update_cnt=0, scan_cnt=1000937, lower_bound_cnt=0, delete_cnt=0
design philosophy:
- 单 b 树没有前途,尝试多数据结构结合?
- 对于 workload1,没有插入,考虑优化静态建树(bulk_load)
- 对于 workload3,4,5,考虑 hash, 并且是要在有 insert 的时候做。
V4_1
design philosophy:
先优化静态建树,考虑 CSB+Tree 风格,让 children 连续。
- 先修 find_leaf 单叶/空树边界:if (cur->is_leaf) return static_cast<LeafNode*>(cur);。这是正确性优化,不改布局。
- 不要在插入开始就关闭静态模式;至少推迟到第一次 split_leaf/split_internal。只要没有 split,物理连续性仍然成立。
- 更进一步可以做“静态上层 + 动态下层”的 hybrid:叶子 split 后只禁用最后一跳 CSB,根和高层仍用 children[0] + pos。这对混合 workload 比全局 is_static=false 更有价值,也不改节点布局。
此外,考虑 prefetch B+Tree 风格:
在 find_leaf 和 scan 中预取 internal_node 和 next_leaf。
scan 里可以预取 next_leaf->keys, next_leaf->values,而不只是 next_leaf 指针本身;scan_num=100 时这类小优化能吃到。
method:
- bulk_load 时候将每一层连续排布,children 连续排布,吃到 CSB+Tree 风格的收益。加了 children_base 字段,并且提前 prefetch。在节点静态时候取代 children[pos] 直接转跳,减小 cache miss.
- find_leaf 中 prefetch_internal 提前预取 children_base, keys, values
- scan 中 prefetch values, keys, next_leaf
observation:
- prefetch 只有在之后一段时间之后要用到的操作才有效果,不然可能是负优化
- prefetch_internal 把所有 cache 的 key 都取进来效果并不好
- scan 的 next_leaf 的 cache miss 是主要瓶颈,考虑到 leaf_max=128 但是 scan_num 最大只有 100,提前预取 1 个即可
V4_2
祝老师指出,可以尝试拟合数据集,并给AI充足的上下文来高效 vibe coding。
observation:
尝试发掘数据集构造的方式,并由此来做一些优化:
- 动态 workload 中
init_table_ratio=0.5,但这 50% 是从全量 key 打乱后取样,所以近似是全量分布的随机样本。 - 后续 insert 来自剩余 50% key,key 不重复。
- bulk_load 中,w1/2 的 num 大约 200M 左右,w3456 大约 100M 左右
design philosophy:
- 用 AI 尝试数据结构的加成
- 静态采用线性数组,做 delta b+tree
- 上层学习性索引,下层 b+tree
- 根据 bulkload 来判断是 workload:
- 如果是 bulkload 前面,则把 bulk_load_leaf = leaf_max,起码可以降空间和降树高
- 否则调整因子大小
method:
- 线性数组的 lower_bound 还没有我的 b+tree 快。可能 cache miss 过多,跨度过大。
- 太累了。放弃调教 AI.
- 调整 leaf_max 和 internal_max 收益不如波动,所以选择空间比较小的配置
- 如果 bulkload 是动态,即使根据“后续插入的大小是 bulkload 的大小,且分布近似随机” 这一特性,负载因子调整到 0.4~0.6 除了空间变大没有任何优点。
final:
- 根据 bulk_load_num 来区分 workload,相应调整分配空间和负载因子
- leaf_max=128, internal_max=64
- 负载因子依旧 0.85~0.9,换取空间
reference:
- Effect of Node Size on the Performance of Cache-Conscious B+-trees
- Improving Index Performance through Prefetching
感想:
- 上下文还是很重要,虽然真很浪费 token。
- AI 在具体实现上可能不如人工精细(比如 prefetch / 指令集会乱用),但是如果提出新的结构,将是很厉害。
- 要成为最会使用 AI 的人。