CMU15-445 24fall Primer HyperLogLog
P1 HyperLogLog注意事项
我的实现地址:CMU15445: 24fall
1.Hyperloglog.cpp/.h
1. 防止内存溢出有关的错误
在默认的构造函数中需要定义寄存器的大小
//hyperloglog.h /** @todo (student) can add their data structures that support HyperLogLog */ int16_t n_bits_; // b int32_t num_registers_; // m=2^b std::vector<uint8_t> registers_; // m registers or m buckets std::mutex mtx; std::shared_mutex shlock_;
//.cpp 定义大小 template <typename KeyType> HyperLogLog<KeyType>::HyperLogLog(int16_t n_bits) { cardinality_ = 0; if (n_bits < 0) n_bits = 0; n_bits_ = n_bits; num_registers_ = (1 << n_bits); registers_.resize(num_registers_, 0); }
2.bitset
的存储问题 和计算问题
bitset<9> bit_(9); 这是他的二进制:000001001 index: 876543210 数值位: 000001001 也就是说他和普通的数组不一样 不是从低到高 而是从高到低 因此PositionOfLeftmostOne函数要求的计算从左到右的1需要倒着计算
我在计算Position
时传入的是完整的 bitset
因此他的大小是BITSET_CAPACITY
//计算position template <typename KeyType> auto HyperLogLog<KeyType>::PositionOfLeftmostOne(const std::bitset<BITSET_CAPACITY> &bset) const -> uint64_t { /** @TODO(student) Implement this function! */ //取消掉前面的nbits_位 直接从有效位开始计算 for (int64_t i = BITSET_CAPACITY - 1 - n_bits_; i >= 0; --i) { if (bset[i] == 1) return static_cast<uint64_t>(BITSET_CAPACITY - n_bits_ - i); } return BITSET_CAPACITY - n_bits_ + 1; }
虽然bitset
是从高位到地位进行的存储 但是前面的n_bits_
位依然是当前寄存器的 index
,如何计算呢? 我们考虑位运算的左移右移 也就是
uint64_t j = (binary >> (BITSET_CAPACITY - n_bits_)).to_ullong(); //桶的编号 //binary是哈希后的64位bitset //to_ullong()是转换为10进制的函数 binary >> (BITSET_CAPACITY - n_bits_) //一共有BITSET_CAPACITY位 出去n_bits_位 剩下的就是有效位 我们把 有效位右移 剩下的就是n_bits_位
3.添加遇到的问题
有基本的互斥锁的知识了解到,AddElem
是一个写入资源的操作 因此我们需要一个互斥锁保护线程 提前在.h
文件添加std::mutex mtx;
成员即可,记得引入头文件#include <mutex>
整个文件都会经常用到强制类型转换的操作 static_cast<TYPE>(Value)
的操作必不可少 registers_[j] = std::max(registers_[j], static_cast<uint8_t>(p))
更是重要
template <typename KeyType> auto HyperLogLog<KeyType>::AddElem(KeyType val) -> void { /** @TODO(student) Implement this function! */ hash_t hash = CalculateHash(val); auto binary = ComputeBinary(hash); //保留n_bits 位 即 桶的编号 uint64_t j = (binary >> (BITSET_CAPACITY - n_bits_)).to_ullong(); //桶的编号 uint64_t p = PositionOfLeftmostOne(binary); //计算1的位置 //他的前面的n_bits_位是记录他的寄存器的位置 x std::lock_guard<std::mutex> lock(mtx); //写入操作 registers_[j] = std::max(registers_[j], static_cast<uint8_t>(p)); }
4.计算遇到的问题
ComputeCardinality
是一个典型的读操作 需要一个std::shared_mutex
来保护,自然的使用 std::shared_lock<std::shared_mutex> guard(shlock_)
,在成员里添加std::shared_mutex shlock_;
即可 不过注意需要引入#include <shared_mutex>
的头文件 ,shared_mutex
是cpp17
的东西
注意寄存器的数量不能为0 和为负数
//注意强制类型转换和浮点数取整的问题 template <typename KeyType> auto HyperLogLog<KeyType>::ComputeCardinality() -> void { /** @TODO(student) Implement this function! */ //读操作 std::shared_lock<std::shared_mutex> guard(shlock_); double sum = 0.0; if (num_registers_ == 0) return; for (int32_t j = 0; j < num_registers_; ++j) { sum += 1.00 / std::pow(2, static_cast<double>(registers_[j])); } double E = CONSTANT * num_registers_ * num_registers_ / sum; cardinality_ = static_cast<size_t>(std::floor(E)); }
2.Hyperloglog_presto.cpp/.h
1.头文件需要添加的元素
int16_t n_leading_bits_; // b int32_t num_registers_; // 寄存器的数量 std::mutex mtx_; // write 的锁 std::shared_mutex shlock_; // 读的锁
2.初始化注意EdgeCase
可能给出的值是个负数 注意要构造足够大的vector
否则会报错
template <typename KeyType> HyperLogLogPresto<KeyType>::HyperLogLogPresto(int16_t n_leading_bits) { if (n_leading_bits < 0) { n_leading_bits = 0; } n_leading_bits_ = n_leading_bits; num_registers_ = 1 << n_leading_bits_; dense_bucket_.resize(num_registers_); cardinality_ = 0; }
3.overflow_bucket_
和dense_bucket_
的计算需要注意 可以直接使用位运算,在查找右侧0的数量时 朴素的写法也可以实现
template <typename KeyType> auto HyperLogLogPresto<KeyType>::AddElem(KeyType val) -> void { /** @TODO(student) Implement this function! */ const hash_t hash_val = CalculateHash(val); std::bitset<64> binary(hash_val); size_t j = (binary >> (64 - n_leading_bits_)).to_ulong(); int64_t tot = 0; // 手写一个函数朴素查找从右到左的0的数量的 lambda 函数 auto find_first_set = [&tot, this](const std::bitset<64> &bits) { for (size_t i = 0; i <= bits.size() - 1 - n_leading_bits_; ++i) { if (!bits[i]) { tot++; } else { break; } } tot = tot != 64 ? tot : (64 - n_leading_bits_); return tot; }; tot = find_first_set(binary); int64_t old_value = dense_bucket_[j].to_ullong(); // 计算原来的值 也就是低四位加上高三位 if (overflow_bucket_.find(j) != overflow_bucket_.end()) { old_value += ((overflow_bucket_[j].to_ullong()) << DENSE_BUCKET_SIZE); } int64_t new_value = std::max(old_value, tot); auto overflow_val = (new_value >> DENSE_BUCKET_SIZE); std::lock_guard<std::mutex> lock(mtx_); // write lock if (overflow_val > 0) { overflow_bucket_[j] = overflow_val; new_value = new_value - (overflow_val << DENSE_BUCKET_SIZE); dense_bucket_[j] = new_value; return; } dense_bucket_[j] = new_value; /* 这是一个不太好的写法 没有检查overflow_bucket_[j]是否存在 而是直接进行了调用 old_value += ((overflow_bucket_[j].to_ullong()) << DENSE_BUCKET_SIZE); int64_t new_value = std::max(old_value, tot); overflow_bucket_[j] = overflow_val; new_value = new_value - (overflow_val << DENSE_BUCKET_SIZE); dense_bucket_[j] = new_value; 同理 计算cardinality_的循环里面也可以不检查 是否存在 不启用这个检查 if(overflolw_bucker_.find(j) != overflolw_bucker_.end() ) 直接调用 for (int j = 0; j < m; ++j) { int64_t val = dense_bucket_[j].to_ullong(); val += overflow_bucket_[j].to_ullong() << DENSE_BUCKET_SIZE; sum += 1.0 / std::pow(2, val); } */ }
3.基数的计算和之前一样 把overflow
移到高三位然后和dense
加起来就出结果了
template <typename T> auto HyperLogLogPresto<T>::ComputeCardinality() -> void { /** @TODO(student) Implement this function! */ std::shared_lock<std::shared_mutex> guard(shlock_); // read lock double sum = 0.0; int m = dense_bucket_.size(); if (m == 0) { return; } for (int j = 0; j < m; ++j) { int64_t val = dense_bucket_[j].to_ullong(); if (overflow_bucket_.find(j) != overflow_bucket_.end()) { val += overflow_bucket_[j].to_ullong() << DENSE_BUCKET_SIZE; } sum += 1.0 / std::pow(2, val); } cardinality_ = static_cast<size_t>(std::floor(CONSTANT * m * m / sum)); }
3.格式的注意
make check-format #工具会输出代码格式不符合规范的地方,需要手动修复或运行 make format 自动修正 make format #format 目标将自动纠正代码。 make check-clang-tidy-p0 #check-lint 和 check-clang-tidy 目标将打印出必须手动修复以符合风格指南的错误。 make check-lint #运行后,工具会输出不符合编码风格的地方,需要手动修复。
4.提交
make submit-p0 #会生成压缩包 提交到gradescope就好了 #如果是新版本需要修改CmakeList set(P0_FILES "src/include/primer/hyperloglog.h" "src/include/primer/hyperloglog_presto.h" "src/primer/hyperloglog.cpp" "src/primer/hyperloglog_presto.cpp" ) #然后删除zip重新压缩 具体可以去翻官方repo的修改记录 #在discord的P0也提到了这个问题
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具