CMU15445 2020 B+TREE简单记录
前期准备
做完了2021的15445,想做一下2020的b+ tree。按照2020 c++ primer assignment步骤一样拉取仓库,安装依赖包,但拉取的代码已经是最新的2021,所以需要将commit回滚到之前的版本。用pro1做测试,回滚到有buffer_pool_manager.cpp的版本。
commit:f92ef74d8fb0d20d2038495b83b3ad0535b25f2c
图中为vscode插件git graph
回滚完之后cmake报以下错误:
1 |
|
将 build_support中gtest_CMakeLists.txt.in的master改成main
完整的流程为:
1 |
|
2020 buffer pool只需实现lru replacement policy和buffer pool manager,涉及的文件有:
src/include/buffer/lru_replacer.h
src/buffer/lru_replacer.cpp
src/include/buffer/buffer_pool_manager.h
src/buffer/buffer_pool_manager.cpp
而2021需实现 lru replacement policy,buffer pool manager instance,parallel buffer pool manager,涉及的文件有:
src/include/buffer/lru_replacer.h
src/buffer/lru_replacer.cpp
src/include/buffer/buffer_pool_manager_instance.h
src/buffer/buffer_pool_manager_instance.cpp
src/include/buffer/parallel_buffer_pool_manager.h
src/buffer/parallel_buffer_pool_manager.cpp
复制粘贴2021代码,简单修改AllocatePage DeallocatePage使用方式就可以通过本地测试了。
通过课程代码:5VX7JZ添加2020的课程进行在线测试
All of the source code for the projects are available on Github. There is a Gradescope submission site available to non-CMU students (Entry Code: 5VX7JZ). We will make the auto-grader for each assignment available to non-CMU students on Gradescope after their due date for CMU students. In exchange for making this available to the public, we ask that you do not make your project implementations public on Github or other source code repositories.
参考:
CMU15445 lab0 C++ PRIMER
googletest pull fails
FAQ
check point1简单记录
因为听说这个实验很难,所以先在b站看完
- Lecture #07: Trees Indexes I
- Lecture #08: Trees Indexes II
- Lecture #09: Index Concurrency Control
然后对照着schedule中note系统学一下b+ tree,实际上这也是我第一次看15445的课。
刚开始的时候不急着实现各个page类的方法,先实现b_plus_tree类的方法,这样就能知道各个page中的方法的作用了,然后再一一实现,实际上通过各类中的API就能看出各个操作的大致流程了。
在模板方法中使用auto自动推导类型的变量,在vscode就不能自动补全,非常不方便,所以尽量用明确的类型了。
在check point1中也不知道啥时候可以unpin,故直接记录所有new或者fetch的页,操作结束前全部释放。(错误的方法)
1 |
|
check point2应该是需要改这个的,但现在无所谓,先实现基本的代码逻辑再考虑优化的问题。
不知道为什么,提交到gradescope显示以下错误,就直接使用本地的grading_b_plus_tree_checkpoint_1_test.cpp进行测试了。
1 |
|
解决:后面发现只需要把begin end isEnd Begin End IsEnd方法全部定义一下就可以了,不需要考虑语法风格检查问题。 The left operand of ‘==’ is a garbage value问题可以通过将src/include/storage/page/tmp_tuple_page.h 加入压缩包解决,不过需要注意的是,tmp_tuple_page.h有可能需要修改
1 |
|
1 |
|
实现的时候对照着可视化网站写比较容易想出相应的代码步骤B+ Tree Visualization (usfca.edu),我觉得挺巧妙的一点就是:内部节点第一个key不用,在分裂的时候移动一半的kv到新节点,正好这个key可以插入到父节点。
1 |
|
遇到的问题:
一 git下载gtest总是会出现各种问题,可能的解决方法有
- 关闭电脑代理,重置git代理
1
2git config --global --unset http.proxy
git config --global --unset https.proxy - 修改hosts文件,添加github与ip地址映射
- 重启网络或主机
相关链接:https://github.com/521xueweihan/GitHub520
二 定义辅助方法创建新的叶子节点
1 |
|
报错need ‘typename’ before because is a dependent scope
原因在于编译器无法识别BPlusTree<KeyType, ValueType, KeyComparator>::LeafPage这个名称是一个成员变量还是一个类型.
故定义宏当做函数返回类型
1 |
|
编译错误need ‘typename’ before *** because *** is a dependent scope 浅析
三 在CopyNFrom函数中,我想直接调用memcpy函数进行拷贝,但报以下错误:
1 |
|
也就是说std::pair<GenericKey<32>, int>类型不是拷贝不变(trivially copyable)类型,对该类型数据拷贝结果未定义
对于未定义行为这篇博客浅谈 C++ Undefined Behavior讲的很好。
undefined behavior 是那些标准没有明确规定、不要求每个 C++ implementation 在其文档中明确规定、且标准也没有对具体的 behavior 施加任何限制的行为。从 abstract machine 的角度考虑,undefined behavior 与 unspecified behavior 也类似,它规定了 abstract machine 的非确定性状态转移:abstract machine 从一个初始的状态开始,执行一个包含 undefined behavior 的程序,abstract machine 的最终状态可能是任何一个状态。标准没有对 abstract machine 的最终状态施加任何限制。经典的 undefined behavior 包括:数组索引越界、null pointer / dangling pointer 解引用、有符号整数上下溢等。
综上所述,规定 undefined behavior 的原因归根结底就是现实世界太复杂了。Undefined behavior 是极度简洁的语言设计和极其复杂的真实世界之间的不可调和的矛盾的产物。
四:默认参数问题
BPlusTree类的构造函数中叶节点和内部节点都使用了宏,但内部节点的MappingType不应该是std::pair<KeyType, ValueType>,而是std::pair<KeyType, page_id_t>
1 |
|
故定义常量
1 |
|
check point2简单记录
删除
在实现删除操作时,突出一个不知道在写啥,也不知道提供的接口各参数如何使用,特别是Coalesce函数的双重指针,后面才知道原来15445还有教材,也就是《数据库系统概念》,这本书上已经有了插入删除的伪码,对着伪码实现起来就非常简单了。不过中文版有些错误,对照英文版比较好。
《Database-System-Concepts》
注意的点:
根节点:首先处理目标节点为根节点的情况
叶子节点:注意this与recipient的位置关系,更新next id
内部节点:注意在移动时mid key的处理,维持不变量,另外对更改移动节点的parent id(创建辅助函数封装该过程)
1 |
|
父节点:对应key的更新与删除
迭代器
为避免多次访问叶子节点,直接一次读取叶子节点所有数据项
1 |
|
并发
写这部分代码我主要参考了:
CMU 15445 Project2 B+TREE | 简单的谈一谈B+树
CMU 15445 Project 2C 实现B+树并发INDEX
实际上也就是用到了第一篇博客中的虚拟根节点与第二篇博客中对unpin与delete加上断言的思路。
虚拟根节点:
1 |
|
想象根节点之上还有一个虚拟根节点,读操作时先获取虚拟根节点的读锁再访问根节点,获取到根节点的读锁后再释放虚拟根节点。插入操作时先获取虚拟根节点的写锁再访问根节点,待子节点安全后统一释放,可压入一个空指针作为虚拟根节点的标记,一个非常巧妙的思路。
1 |
|
而第二篇分析太多了,懒得看,主要是借鉴了断言的用法来验证实现的正确性
1 |
|
之前实现没怎么考虑unpin的问题,直接在辅助函数中记录create或fetch的页,而后统一unpin。这给我这部分的实现埋了许多的坑。
读取操作与插入操作实现起来没有太大的问题,而删除操作中可能发生页的删除,故修改CoalesceOrRedistribute函数定义,传递std::stack<Page *> *ancestors参数,每发生一次节点的删除就将当前页弹出,表示当前页的解锁与unpin不再由Remove函数负责,而由AdjustRoot函数或Coalesce函数负责(Coalesce函数可能发生节点交换,故不一定删除当前页,有可能删除其兄弟节点,故职责传递给函该函数编写代码比较方便)。写代码时调用fetch或create时一定要明确由哪个函数负责unpin。
1 |
|
可在创建页或删除页时打印相关信息,便于了解程序的执行状态;也可以利用BPlusTree的Print方法输出b+树的数据构成。加锁实现完后可以跑一遍单线程的程序,保证代码基本的正确性。
遇到的问题:
1 在基本实现后运行顺序执行的测试程序时删除根节点的pin_count总是等于1。
1 |
|
在GetValue时pin count等于0,Remove时就变成1。后面才发现for (auto pair : tree) 隐含调用了begin方法,而在FindLeafPage方法中使用了FetchInternalPage方法,忘了unpin。
2 unpin时is_dirty问题
在顺序测试时,如果顺序删除,则结果正确,如果打乱,则结果错误。
1 |
|
后面发现在ReleaseAncestorsPage函数中UnpinPage我一律设置为了false,实际上当由于子节点安全调用的ReleaseAncestorsPage,is_dirty应该是false,因为当前操作并不会修改这些节点的值,但在插入或删除之后调用的ReleaseAncestorsPage应该设置为true。
1 |
|
3: 节点安全问题
1 |
|
并行执行执行时,总有一些数组(512)会出现在较小的节点上,通过在每次分裂时打印B+树的信息发现,错误总是发生在分裂前后,而后检查InsertIntoLeaf函数代码,发生判断节点是否安全的代码存在问题:
1 |
|
如果当前节点大小恰好为max_size-1,在插入一元素后将会发生分裂,进而修改父节点的值,故在这个时候不应该释放父节点的锁。
故修改为:
1 |
|
4 MixTest测试时unpin和delete的断言失败
后面发现Page的id与TreePage的id有时候会不一样(例如上面的id 0),故unpin一律用TreePage的page id
但还是没有彻底解决整个问题,Page的数据很奇怪,id=0,is_dirty为123
暂时没找到原因,以后有时间再看看。不过还是能通过所有测试