CMU-15445(2024fall)——PROJECT#0
题目介绍
这是题目原文。
注意:在拉取项目的时候需要注意拉取2024fall的Tag,本人血泪教训!
本题是关于HyperLogLog的具体实现,其介绍可以看这个视频进行了解。简单来说HyperLogLog可以在一个非常小的固定内存下(一般为十几KB)非常快速地估算出大量数据的唯一元素数量,并且其可以分别对多个数据库独立估算,还不会损失精度。
比如要统计访问网站的唯一用户数,如果用精确算法,需要对数据库中的所有数据进行比对,这会占用非常大的内存且非常耗时,尤其是在分布式存储的情况下。但我们在绝大多数的时候并不需要一个精确的值,因此可以使用HLL对其进行估算(虽说是估算,但其误差也非常小)。
这道题主要检查了学生的C++基础,以及对C++项目的开发和调试能力。
文章末尾有本人的项目代码地址,文章内容主要讲述思路和一些细节不会出现代码,大家各取所需。
Task#1
Task#1是实现基本的HLL算法。
所需修改的文件目录为:
src/include/primer/hyperloglog.h
src/primer/hyperloglog.cpp
测试文件的目录为:
test/primer/hyperloglog_test.cpp
原始代码中已经给出了一些变量和方法的定义及实现,需要我们进行完善和补全。
hyperloglog.h中的变量部分给出了最终的结果cardinality_和计算常量CONSTANT,缺少了题目中所提及的b、m、p,b和p可以设置为uint8_t类型的参数,因为其值的范围在0到63之间,m可以设置为uint8_t的数组,原因同上。
CalculateHash():将传入的值转换为哈希值的方法。GetCardinality():返回最终结果。
hyperloglog.cpp是该算法主要的函数实现。以下是各个方法的简介:
HyperLogLog(inital_bits):构造函数,需要我们根据传入的b初始化我们内部的b和m。AddElem(val):计算出传入值的p并将其存在m中(最主要方法)。ComputeCardinality():根据公式计算最终结果。ComputeBinary():将哈希值转换为64位二进制流。PositionOfLeftmostOne():计算出p值(二进制流中最左侧 1 的位置)。
我这里添加了一个函数GetBucketValue(),用来计算p应该存在m的哪个位置中。
接下来我会大概说一下我的解答中每个方法的实现。
HyperLogLog(inital_bits)
根据传入的b初始化我们内部的b和m。需要对b的值进行判断,看是否是小于0的值,如果小于0则直接return,否则对我们内部的b进行赋值。同时需要根据b重新设置我们m的大小。
注意: 如果将b设置为size_t或者unit_t类型,需要注意其数值不会小于0,容易因此产生bug。
ComputeBinary()
可以直接使用std::bitsize<64>()方法,将传入的哈希值转换为64位二进制流。需注意,这里转换后的值第0位是低位,第63位是高位,不要搞反了。
GetBucketValue()
我们需要从后往前获取b位二进制流的内容,计算这个二进制数在十进制下是几。可以通过位操作符<<进行计算。
PositionOfLeftmostOne()
由于Task#1是计算高位1的位置,高位的前b位又被用来计算插入位置。所以我们需要从第63 - b位开始,从高到低查找二进制流第一个1的位置。
AddElem(val)
有了前面几个方法,这个方法实现起来就比较简单了。分别调用前面的几个方法,得到参数的哈希值、64位二进制流、插入m的位置、高位1的位置p,就只需将当前p和m上的值比较一下大小,将大的存入就可以了。
ComputeCardinality()
根据公式计算最终结果,可以先算出分母的总数,再计算整个公式,可以使用static_cast<size_t>()方法保留结果的整数部分。进行幂计算时可以使用1.0 / std::pow(2, num)进行计算。
Task#2
要求我们实现HyperLogLog算法的密集布局,整体与Task#1类似,不过这里是从低位开始计算0的数量,并且如果数量超过15,需要将其拆分成高3位和低4位分别进行存储。
所需修改的文件目录为:
src/include/primer/hyperloglog_presto.h
src/primer/hyperloglog_presto.cpp
hyperloglog_presto.h文件多出了dense_bucket_和overflow_bucket_两个参数,前者和前面的m基本相同,后者是在发生溢出时存储高3位的桶。
整体思路没有什么变化,只有以下几点需要变化:
PositionOfLeftmostOne()方法需要改为从低到高计算0的个数。- 获取到0的个数
p后,需将其拆分为高3位和低4位。可以使用与操作符&进行处理。 - 在对比
p和插入位置的值,以及计算最终结果时,要将dense_bucket_和overflow_bucket_中相应位置的值进行合并,转换为十进制进行比较。转换为十进制可以使用to_ulong()方法,合并操作可以使用或操作符|。
测试
先将test/primer/hyperloglog_test.cpp中的测试函数第二个参数的DIABLE_去掉。
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make -j$(nproc) hyperloglog_test
./test/hyperloglog_test
正常应该如图所示:

提交
cd ..
mkdir build_rel && cd build_rel
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
make format
make check-clang-tidy-p0
make submit-p0
如果有格式问题clang-tidy会报错。我这里本地没有报错,但是提交到平台上就会报格式错误,可能是工具版本的问题。
执行完成后会在根目录生成一个压缩包,上传该压缩包等待平台打分。
正常如图所示:

代码地址:https://github.com/GCW-kuku/bustub
