bloom filter

举报
nullptr; 发表于 2026/07/16 17:18:27 2026/07/16
【摘要】 /* * practice31_bloom_filter.c — Bloom Filter (PG bloomfilter.c 简化版) * ===================================================== * 对应 PG 模块: * src/backend/lib/bloomfilter.c — 通用 bloom filter 实...
/*
 * practice31_bloom_filter.c — Bloom Filter (PG bloomfilter.c 简化版)
 * =====================================================
 * 对应 PG 模块:
 *   src/backend/lib/bloomfilter.c        — 通用 bloom filter 实现
 *   src/backend/executor/nodeHashjoin.c  — hash join 用 bloom filter 优化
 *   contrib/bloom/                       — bloom 索引扩展
 *
 * 为什么重要:
 *   Bloom filter 是概率数据结构: 能确认"肯定不存在", 但只能说"可能存在"。
 *   PG 用它:
 *     - hash join: 先用 bloom filter 过滤 probe 端, 减少不必要的 hash 查找
 *     - parallel seqscan: worker 之间用 bloom filter 同步"已扫过的块"
 *     - bloom index: 索引本身就是一个 bloom filter
 *   特点: 有假阳性 (false positive), 无假阴性 (false negative)。
 *
 * 核心原理:
 *   1. 位数组 (bit array): m 个 bit, 初始全 0
 *   2. k 个哈希函数: 把元素映射到 [0, m) 的 k 个位置
 *   3. 插入: 算 k 个位置, 全部置 1
 *   4. 查询: 算 k 个位置, 全为 1 → "可能存在"; 有 0 → "肯定不存在"
 *
 * 位数组实现 (本题重点):
 *   用 uint8_t bits[] 存储, 每个字节 8 位
 *     bit i 在 bits[i / 8] 的第 (i % 8) 位
 *     set: bits[i/8] |=  (1 << (i % 8))
 *     get: (bits[i/8] >> (i % 8)) & 1
 *
 * 难度: ★★☆☆☆
 *
 * 学习目标:
 *   1. 位运算 (<< >> | & ~) 的实战使用
 *   2. 位数组 (bit array) 的索引映射 (字节/位)
 *   3. 概率数据结构思维 (假阳性/假阴性)
 *   4. 哈希函数基础 (乘法哈希)
 *
 * 编译: gcc -Wall -Wextra -O2 -o practice31 practice31_bloom_filter.c
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <assert.h>

/* ============================================================
 * 常量
 * ============================================================ */

#define BLOOM_BYTES     256     /* 位数组字节数 (256 * 8 = 2048 位) */
#define BLOOM_BITS      (BLOOM_BYTES * 8)
#define BLOOM_K         3       /* 哈希函数个数 */

/* ============================================================
 * 数据结构
 * ============================================================ */

typedef struct {
    uint8_t  bits[BLOOM_BYTES];   /* 位数组 */
    size_t   n_added;             /* 已插入元素数 (统计用) */
} BloomFilter;

/* ============================================================
 * 哈希函数 (已实现)
 *   乘法哈希: h = seed, 每个字节 h = h * 31 + byte
 *   不同 seed 产生不同哈希 (模拟 k 个独立哈希函数)
 * ============================================================ */

static uint32_t bloom_hash(const void *data, size_t len, uint32_t seed) {
    const uint8_t *p = (const uint8_t *)data;
    uint32_t h = seed;
    for (size_t i = 0; i < len; i++) {
        h = h * 31 + p[i];
    }
    return h;
}

/*
 * 用第 k 个哈希函数算出 bit 位置 (0 .. BLOOM_BITS-1)
 *   k = 0, 1, ..., BLOOM_K-1
 */
static uint32_t bloom_hash_pos(const void *data, size_t len, int k) {
    /* 不同 seed 模拟不同哈希函数 */
    uint32_t seed = (k + 1) * 2654435761u;   /* Knuth 乘法常数 */
    uint32_t h = bloom_hash(data, len, seed);
    return h % BLOOM_BITS;
}

/* ============================================================
 * 位数组操作 (核心! 重点练位运算)
 * ============================================================ */

/*
 * 设置第 i 位为 1
 *   对应 PG 的 bloom_filter.c 里的位操作 (用 word 数组, 本题用 byte 数组简化)
 *
 * 实现:
 *   - 字节索引: byte_idx = i / 8
 *   - 位偏移:   bit_idx  = i % 8
 *   - 设置:     bits[byte_idx] |= (1 << bit_idx)
 *
 * 图示 (i = 10):
 *   10 / 8 = 1  → bits[1]
 *   10 % 8 = 2  → 第 2 位
 *   bits[1] |= (1 << 2)  即 bits[1] |= 0b00000100
 *
 *        bits[0]              bits[1]
 *   [b7 b6 b5 b4 b3 b2 b1 b0] [b7 b6 b5 b4 b3 b2 b1 b0]
 *                                ↑ 设置这一位 (bit 2)
 *   bit 0 在最右 (最低位), bit 7 在最左
 */
void bloom_set_bit(BloomFilter *bf, uint32_t i) {
    assert(1 < BLOOM_BITS);
    uint32_t byte_idx = i / 8;
    uint32_t bit_idx = i % 8;
    bf->bits[byte_idx] |= (1 << bit_idx);
}

/*
 * 获取第 i 位的值 (0 或 1)
 *
 * 实现:
 *   - byte_idx = i / 8
 *   - bit_idx  = i % 8
 *   - return (bf->bits[byte_idx] >> bit_idx) & 1
 *
 * 为什么用 >> 再 & 1?
 *   - >> bit_idx 把目标位移到最低位
 *   - & 1 只保留最低位, 屏蔽其他位
 *   例: bits[1] = 0b00000100, 取 bit 2:
 *       0b00000100 >> 2 = 0b00000001
 *       0b00000001 & 1  = 1
 */
int bloom_get_bit(const BloomFilter *bf, uint32_t i) {
    uint32_t byte_idx = i / 8;
    uint32_t bit_idx = i % 8;
    return (bf->bits[byte_idx] >> bit_idx) & 1;
}

/* ============================================================
 * Bloom Filter API
 * ============================================================ */

/*
 * 初始化 (清零位数组)
 *   对应 PG 的 bloom_init
 */
void bloom_init(BloomFilter *bf) {
    memset(bf->bits, 0, BLOOM_BYTES);
    bf->n_added = 0;
}

/*
 * 插入元素
 *   对应 PG 的 bloom_add_element
 *
 * 流程:
 *   - 用 BLOOM_K 个哈希函数算出 k 个位置
 *   - 每个位置 bloom_set_bit 置 1
 *   - n_added++
 */
void bloom_add(BloomFilter *bf, const void *data, size_t len) {
    for(int k=0; k<BLOOM_K; k++)
    {
        bloom_set_bit(bf, bloom_hash_pos(data, len,k));
    }
    bf->n_added++;
}

/*
 * 查询元素是否"可能存在"
 *   对应 PG 的 bloom_contains
 *
 * 流程:
 *   - 用 BLOOM_K 个哈希函数算出 k 个位置
 *   - 若所有位置都是 1 → 返回 true (可能存在, 可能假阳性)
 *   - 若有任一位置为 0 → 返回 false (肯定不存在)
 *
 * 注意: 返回 true 不代表一定存在 (假阳性), 返回 false 代表一定不存在
 */
bool bloom_might_contain(const BloomFilter *bf, const void *data, size_t len) {
    for(int k=0; k<BLOOM_K; k++)
    {
        if(bloom_get_bit(bf,bloom_hash_pos(data,len,k)) == 0)
        {
            return false;
        }
    }
    return true;
}

/* ============================================================
 * 工具函数 (已实现)
 * ============================================================ */

static void bloom_dump(const BloomFilter *bf, const char *label) {
    printf("--- %s ---\n", label);
    printf("  n_added = %zu, bits = %d, k = %d\n",
           bf->n_added, BLOOM_BITS, BLOOM_K);
    int set_bits = 0;
    for (int i = 0; i < BLOOM_BITS; i++) {
        if (bloom_get_bit(bf, i)) set_bits++;
    }
    printf("  set bits = %d / %d (%.1f%%)\n",
           set_bits, BLOOM_BITS, 100.0 * set_bits / BLOOM_BITS);
}

/* ============================================================
 * 测试
 * ============================================================ */

static void test_bit_ops(void) {
    printf("=== test_bit_ops ===\n");
    BloomFilter bf;
    bloom_init(&bf);

    /* 初始全 0 */
    for (int i = 0; i < BLOOM_BITS; i++) {
        assert(bloom_get_bit(&bf, i) == 0);
    }
    printf("  初始全 0: OK\n");

    /* 设置几个位 */
    bloom_set_bit(&bf, 0);
    bloom_set_bit(&bf, 7);
    bloom_set_bit(&bf, 8);
    bloom_set_bit(&bf, 100);
    bloom_set_bit(&bf, 2047);

    assert(bloom_get_bit(&bf, 0) == 1);
    assert(bloom_get_bit(&bf, 7) == 1);
    assert(bloom_get_bit(&bf, 8) == 1);
    assert(bloom_get_bit(&bf, 100) == 1);
    assert(bloom_get_bit(&bf, 2047) == 1);

    /* 相邻位不受影响 */
    assert(bloom_get_bit(&bf, 1) == 0);
    assert(bloom_get_bit(&bf, 6) == 0);
    assert(bloom_get_bit(&bf, 9) == 0);
    assert(bloom_get_bit(&bf, 99) == 0);
    assert(bloom_get_bit(&bf, 2046) == 0);

    printf("  设置 bit 0,7,8,100,2047 后读取: OK\n");

    /* 重复设置同一 bit 不影响 */
    bloom_set_bit(&bf, 0);
    bloom_set_bit(&bf, 0);
    assert(bloom_get_bit(&bf, 0) == 1);
    printf("  重复设置同一 bit: OK (幂等)\n");

    printf("  PASS\n\n");
}

static void test_add_and_query(void) {
    printf("=== test_add_and_query ===\n");
    BloomFilter bf;
    bloom_init(&bf);

    /* 插入几个字符串 */
    const char *items[] = {"hello", "world", "postgres", "bloom"};
    for (int i = 0; i < 4; i++) {
        bloom_add(&bf, items[i], strlen(items[i]) + 1);
    }
    bloom_dump(&bf, "插入 4 个元素后");

    /* 查询已插入的: 应该都返回 true */
    for (int i = 0; i < 4; i++) {
        bool found = bloom_might_contain(&bf, items[i], strlen(items[i]) + 1);
        assert(found);
        printf("  \"%s\" → 可能存在: %s\n", items[i], found ? "YES" : "NO");
    }

    printf("  PASS\n\n");
}

static void test_true_negative(void) {
    printf("=== test_true_negative ===\n");
    BloomFilter bf;
    bloom_init(&bf);

    /* 只插入 "hello" */
    bloom_add(&bf, "hello", 6);

    /* 查询 "world" (没插入过): 应该返回 false (肯定不存在) */
    bool found = bloom_might_contain(&bf, "world", 6);
    assert(!found);
    printf("  \"world\" (未插入) → 肯定不存在: %s\n", found ? "YES" : "NO");

    /* 查询 "postgres" (没插入过) */
    found = bloom_might_contain(&bf, "postgres", 9);
    assert(!found);
    printf("  \"postgres\" (未插入) → 肯定不存在: %s\n", found ? "YES" : "NO");

    printf("  PASS (无假阴性)\n\n");
}

static void test_false_positive_rate(void) {
    printf("=== test_false_positive_rate ===\n");
    BloomFilter bf;
    bloom_init(&bf);

    /* 插入 50 个元素 */
    char buf[32];
    for (int i = 0; i < 50; i++) {
        snprintf(buf, sizeof(buf), "item_%d", i);
        bloom_add(&bf, buf, strlen(buf) + 1);
    }
    bloom_dump(&bf, "插入 50 个元素后");

    /* 查询 1000 个未插入的元素, 统计假阳性 */
    int false_positives = 0;
    for (int i = 1000; i < 2000; i++) {
        snprintf(buf, sizeof(buf), "item_%d", i);
        if (bloom_might_contain(&bf, buf, strlen(buf) + 1)) {
            false_positives++;
        }
    }
    printf("  查询 1000 个未插入元素, 假阳性数 = %d (%.1f%%)\n",
           false_positives, false_positives / 10.0);

    /* 假阳性率应该比较低 (< 20%) */
    assert(false_positives < 200);
    printf("  PASS (假阳性率 < 20%%)\n\n");
}

static void test_reset(void) {
    printf("=== test_reset ===\n");
    BloomFilter bf;
    bloom_init(&bf);

    bloom_add(&bf, "test", 5);
    assert(bloom_might_contain(&bf, "test", 5));

    /* 重置 */
    bloom_init(&bf);
    assert(!bloom_might_contain(&bf, "test", 5));
    assert(bf.n_added == 0);
    printf("  重置后所有元素消失: OK\n");

    printf("  PASS\n\n");
}

int main(void) {
    test_bit_ops();
    test_add_and_query();
    test_true_negative();
    test_false_positive_rate();
    test_reset();

    printf("========================================\n");
    printf("practice31 全部通过!\n");
    printf("========================================\n");
    return 0;
}

/*
 * 进阶思考题:
 *
 * 1. 为什么 bloom filter "无假阴性"?
 *    (提示: 插入时所有位置 1, 查询时只要有一个 0 就返回 false;
 *           已插入的元素所有位都是 1, 不会返回 false)
 *
 * 2. 为什么有假阳性?
 *    (提示: 不同元素的哈希可能撞到同一位置, 导致未插入的元素
 *           恰好所有位都被其他元素设成 1)
 *
 * 3. 假阳性率和什么有关?
 *    (提示: (a) 位数组大小 m; (b) 哈希函数个数 k; (c) 已插入元素数 n)
 *    m 越大 → 假阳性越低 (但占内存)
 *    k 太少 → 覆盖不够; k 太多 → 位数组被填满太快
 *    最优 k ≈ (m/n) * ln(2)
 *
 * 4. PG 的 hash join 怎么用 bloom filter?
 *    (提示: build 端建 bloom filter, probe 端先查 bloom filter,
 *           过滤掉肯定不匹配的行, 减少实际 hash 查找)
 *    为什么不用 hash table 直接查? (提示: bloom filter 更小, cache 友好)
 *
 * 5. 为什么用 byte 数组而不是 int 数组存 bit?
 *    (提示: byte 更细粒度, int 可能浪费; 但 int 操作更少次数)
 *    PG 用 uint64 数组 (一次处理 64 位), 为什么? (提示: 性能)
 *
 * 6. 能不能从 bloom filter 删除元素?
 *    (提示: 不能! 因为多个元素可能共享同一 bit, 删一个会影响其他)
 *    Counting bloom filter 用计数器代替 bit, 支持删除, 代价是什么?
 *
 * 7. 本题用 3 个哈希函数 (BLOOM_K=3), 2048 位, 插入 50 个元素。
 *    理论假阳性率约多少?
 *    (公式: (1 - e^(-kn/m))^k = (1 - e^(-3*50/2048))^3 ≈ 0.7%)
 *    和你测出的假阳性率对比?
 *
 * 8. Bloom filter 和 hash table 的区别?
 *    (提示: bloom filter 不存原始数据, 只存 bit; 不能遍历, 不能取值,
 *           只能查"可能存在"; 优势: 极省内存)
 */

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。