CMU15-445 24fall Primer HyperLogLog

P1 HyperLogLog注意事项

Task1

初始化数据结构 n位和2^n个桶

除了n位之后的,按要求从左到右计算1的位置

对给定val hash

PositionOfLeftmostOne(....) : 它计算最左边的 1 的位置。

register[j] 寄存器j的值

register[j] = max(PositionOfLeftmostOne(j),register[j])

然后对所有bucket用哪个数学公式计算就可以了

Task2

DenseBucket存储后四位

OverFlow存储前三位

注意这里的DenseBuacket 是一个bitset类型的vector

OverflowBucket是一个int到bitset的哈希表

Presto和Tsk1是反过来的 从右边开始查找0的个数

也就是从bitset的低位第0位开始

bitset的存储
7 6 5 4 3 2 1 0
0 1 0 0 0 1 0 1

然后利用位运算的性质去计算old的值

和计算新的值

作比较就可以了

我的实现地址: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_mutexcpp17的东西

注意寄存器的数量不能为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也提到了这个问题
posted @ 2025-02-08 23:38  phrink  阅读(173)  评论(0)    收藏  举报