Page 428 - 《软件学报》2025年第5期
P. 428

2328                                                       软件学报  2025  年第  36  卷第  5  期


                 3.2   点查找
                    如图  3  红色箭头所示, 点查找操作先在索引内部节点中查找给定键, 确定键所在的叶节点地址和元数据地址
                 (①②③). 当查找到达叶节点的上一层内部节点时, 根据该内部节点存储的叶节点的模型参数计算出目标键在叶
                 节点  block  数组中的位置和对应的块元数据地址. 由于避免了局部查找, 每层内部节点的查找只需要访问                            32 B  的
                 数据, 提升了缓存命中率, 每个内部节点中只需访问一个                 PM  块, 节省了  PM  带宽资源. 在叶节点查找键的过程中,
                 首先读取块元数据中的指纹          (④). 如果存在匹配的指纹, 则继续在叶节点           block  中比较该位置处的键      (⑤). 如果元
                 数据的指纹匹配失败, 则跳过叶节点的访问, 直接进入溢出缓存中继续查找                        (⑥⑦). 与  APEX  和  PLIN  相比, 当键
                 在叶节点中不存在时, PLTree 只会产生         1  个缓存行的   DRAM  访问. 而在  APEX  和  PLIN  中, 无论键是否存在于叶
                 节点, 都必须先在叶节点中进行查找, 并产生至少               1  个  PM  块的访问. 通过设置块元数据, PLTree 可以充分利用
                 DRAM  高带宽特性, 减少     PM  访问次数, 合理利用有限的       PM  带宽资源.
                    如图  2  红色箭头所示, 当进入溢出缓存中查找键时, PLTree 首先使用              SIMD  指令在缓存元数据中逐层查找是
                 否存在匹配的指纹       (❶❷). 如果存在指纹匹配, 则继续在溢出缓存中对应的                entry  中从右至左比较指纹匹配的键
                 (❸), 若键相同直接返回结果, 无需继续向左和向下层继续查找, 因为后面的即使存在相同的键也是旧数据. 当键
                 存在时, 溢出缓存中查找操作最少只需进行一个缓存行的                   DRAM  读取和一个    PM  块访问. 当键不存在时, 溢出缓
                 存中的查找开销为       N  个缓存行的   DRAM  访问  (N  为溢出缓存的层数), 而不会产生额外的          PM  访问.

                 3.3   插入更新
                    如图  3  红色箭头所示, 与查找操作的相同, 插入更新操作需要先在内部节点中定位键所在的叶节点以及对应
                 的元数据   (⑧⑭⑨). 在叶节点中, 使用原地更新和日志式插入的方式将键值对写入预测的                       block  中. 在此过程中, 如
                 果溢出缓存中已经存在与待插入键重复的数据, 在后续查询操作中, 这些重复数据将会被视为过期或旧数据.
                 PLTree 对分布不均匀的真实工作负载进行了优化: 避免了插入数据之前在溢出缓存对键进行唯一性检查. 因为在
                 PLTree 中, 只需确保关键字在叶节点中是唯一的, 允许溢出缓存中的键存在重复. 因此, 在插入键值对之前, 只需
                 要在叶节点的     block  中进行键的唯一性检查, 而无需继续在溢出缓存中查找该键. 具体的做法是, 在插入键值对之
                 前, 先在块元数据和叶节点中查找键是否存在              (⑩⑪). 如果键已存在, 则会在       block  中原地更新键值对的值, 由于值
                 的大小为   8 B, 在写入时采用    8 B  原子写入  PM, 并使用内存屏障      SFENCE  指令  [39] 隔离后续的写指令, 因此程序崩
                 溃时不会出现中间状态. 如果键在           block  中不存在, 就以追加的方式将键值对写入到           block  的空闲槽位中   (⑫), 并
                 将键的指纹加入块元数据的指纹数组之中. 键值对采用先写入值                        (8 B) 再写入键   (8 B) 的方式持久化, 使用
                 SFENCE  指令隔离后续的写指令, 确保系统崩溃发生在值写入和键写入之间时, 槽位中的键仍为空闲标志, 避免无
                 效键值对的写入. 上述插入方式避免了额外的              PM  访问, 因为键的唯一性检查只在叶节点中进行.
                    当叶节点产生由冲突导致的溢出时, APEX             和  PLIN  每次只往溢出缓存中写入一个         16 B  大小的键值对   (写放
                 大为  16, 一个  PM  块  256 B/键值对大小  16 B). 针对  PM  中由数据访问粒度不匹配引发的写放大问题, PLTree 进行
                 了优化: 在叶节点     block  产生溢出时选择将整个      block  的数据批量地写入到溢出缓存中          (⑲), 当  PLTree 叶节点中
                 某个  block  中的数据全部写入溢出缓存第         0  层的同一个   entry  时, 写放大最小可降低至     1.07 (一个  PM  块  256 B/
                 block  中的数据  240 B). 通过批量写入数据的方式, 能够根据         PM  的特性, 减小  PM  中的写放大. 此外, 每次批量写
                 入还可以均摊数据持久化和内存屏障指令带来的开销, 从而提升系统的性能表现.
                    批量写入完成后, 需要通过设置空闲标志将叶节点中                  block  槽位标记为空闲, 并清除对应块元数据中的指纹
                 数据. 在批量写入缓存和将         block  槽位设置为空闲时, 即使发生系统崩溃, 也不会影响索引的一致性. 这是因为
                 PLTree 支持存储关键字重复的数据, 可以分为两种情况讨论: (1) 批量写入溢出缓存时发生系统崩溃. 此时可能有
                 一部分键值对已经被写入溢出缓存中, 在叶节点               block  中的键值对未被改变, 因此系统恢复后, 叶节点中的数据仍
                 旧存在, 对查找操作没有影响. 写入溢出缓存中的部分键值对会被当做旧的数据, 下一次批量写入时, 键重复的旧
                 数据会被覆盖. (2) 将叶节点      block  槽位设置为空闲时系统崩溃. 此时        block  中的键值对已经全部写入溢出缓存, 叶
                 节点  block  中可能存在部分键值对未被置空, 由于此时            block  中的键值对已经全部写入溢出缓存, 系统恢复后, 并
   423   424   425   426   427   428   429   430   431   432   433