Phần 1 cho thấy Bloom filter tiết kiệm bộ nhớ khủng khiếp và có tỉ lệ báo nhầm tính trước được. Nhưng "tính trước được" chỉ có ích nếu bạn biết chọn tham số. Bloom có hai nút chỉnh: m (số bit) và k (số hàm băm), và chọn sai cả hai đều làm hỏng: quá ít bit thì báo nhầm cao, quá ít hoặc quá nhiều hàm băm cũng thế. May thay, lý thuyết cho hai công thức gọn để tính m và k tối ưu từ số phần tử và tỉ lệ báo nhầm mong muốn. Bài này (phần 2 loạt Xác suất) đo thật ba điều: có một k tối ưu thật (không phải càng nhiều càng tốt), công thức m cho đúng số bit cần, và một cái bẫy khiến Bloom "hỏng" trong thực tế — nhồi quá số phần tử dự kiến.

Hai công thức thiết kế

Cho trước n (số phần tử sẽ thêm) và p (tỉ lệ báo nhầm bạn chấp nhận), tính:

m = -n · ln(p) / (ln2)²      # số BIT cần thiết
k = (m/n) · ln2              # số HÀM BĂM tối ưu (≈ 0.7 × bit/phần tử)

m/n là số bit mỗi phần tử — nút chỉnh chính. p càng thấp cần càng nhiều bit/phần tử. Và có một điểm phản trực giác: k không phải càng nhiều càng tốt. Nhiều hàm băm hơn nghĩa là mỗi phần tử bật nhiều bit hơn, giúp phân biệt tốt hơn — nhưng cũng làm mảng bit "đầy" nhanh hơn, tăng khả năng trùng. Có một k cân bằng cho báo nhầm thấp nhất.

Ảnh chụp đoạn mã nền tối minh hoạ chọn m và k tối ưu cho Bloom filter từ n phần tử và p mong muốn tính số bit và số hàm băm chuẩn xác, hai công thức thiết kế Bloom cho trước n số phần tử p tỉ lệ báo nhầm mong muốn m bằng trừ n nhân ln p chia ln2 bình phương số bit cần thiết k bằng m chia n nhân ln2 số hàm băm tối ưu khoảng 0.7 nhân bit mỗi phần tử bit mỗi phần tử bằng m chia n p thấp hơn cần nhiều bit mỗi phần tử hơn, vì sao có k tối ưu không phải càng nhiều càng tốt k quá ít ít bit đại diện mỗi phần tử dễ trùng ngẫu nhiên FP cao k quá nhiều bật quá nhiều bit mảng bit nhanh đầy FP cao có một k cân bằng khoảng m chia n ln2 cho FP thấp nhất quét k để thấy rõ, đo quét k trên cùng m n for k trong 1 2 3 5 7 9 11 15 fp bằng measure_fp m k n thêm n phần tử đếm báo nhầm so với 1 trừ e mũ trừ kn chia m mũ k tìm k cho FP nhỏ nhất, quy tắc nhớ nhanh bit mỗi phần tử khoảng 10 bit mỗi phần tử p khoảng 1 phần trăm k bằng 7 khoảng 14 bit mỗi phần tử p khoảng 0.1 phần trăm k bằng 10 khoảng 19 bit mỗi phần tử p khoảng 0.01 phần trăm k bằng 13 mỗi lần giảm p xuống 10 lần thêm khoảng 4.8 bit mỗi phần tử đo thật có khớp

Hình 1: Hai công thức thiết kế Bloom — m = -n·ln(p)/(ln2)² (số bit), k = (m/n)·ln2 (số hàm băm tối ưu); có một k cân bằng cho báo nhầm thấp nhất (quá ít hoặc quá nhiều đều tệ); quy tắc nhớ nhanh ~10 bit/phần tử cho ~1%, ~14 bit cho ~0.1%.

Đo thật: k tối ưu, công thức m, và cái bẫy

Mình dùng Bloom tự cài (như phần 1), chạy ba thí nghiệm trên n=500.000:

Ảnh chụp bảng kết quả chạy thật chọn m k tối ưu Bloom output thật go-lab python Bloom tự cài n bằng 500000, a quét k trên cùng m 10 bit mỗi phần tử có k tối ưu bằng 7 k FP thực FP lý thuyết k bằng 1 9.580 phần trăm 9.516 phần trăm k quá ít FP cao k bằng 3 1.730 phần trăm 1.741 phần trăm k bằng 5 0.949 phần trăm 0.943 phần trăm k bằng 7 0.814 phần trăm 0.819 phần trăm tối ưu khoảng m chia n ln2 bằng 6.9 k bằng 9 0.910 phần trăm 0.913 phần trăm k bằng 11 1.181 phần trăm 1.165 phần trăm k bằng 15 2.272 phần trăm 2.266 phần trăm k quá nhiều bit đầy FP tăng lại, b cho p mục tiêu tính m FP thực đạt đúng mục tiêu p mục tiêu m bit bit mỗi phần tử k FP thực 1.00 phần trăm 4792530 9.6 7 1.012 phần trăm đạt khoảng 1 phần trăm 0.10 phần trăm 7188794 14.4 10 0.102 phần trăm đạt khoảng 0.1 phần trăm 0.01 phần trăm 9585059 19.2 13 0.007 phần trăm đạt khoảng 0.01 phần trăm giảm p 10 lần thêm khoảng 4.8 bit mỗi phần tử công thức đúng, c nhồi quá n dự kiến FP bùng lên Bloom thiết kế cho n bằng 500k m bằng 5 triệu bit k bằng 7 số phần tử thêm FP thực 500000 0.814 phần trăm đúng thiết kế 1000000 13.789 phần trăm gấp đôi FP tăng 17 lần 2000000 64.415 phần trăm gấp 4 gần như vô dụng phải ước lượng n đúng cấp m đủ nhồi quá bằng Bloom đầy bằng FP thảm họa

Hình 2: Chạy thật — (a) quét k trên cùng m (10 bit/phần tử): k=7 cho FP thấp nhất 0.814% (khớp lý thuyết ~6.9), k=1 cho 9.58%, k=15 cho 2.27%; (b) p mục tiêu → m: 1%→9.6 bit/phần tử (FP thực 1.012%), 0.1%→14.4 bit (0.102%), 0.01%→19.2 bit (0.007%); (c) nhồi quá: 500k→0.814%, 1M→13.789%, 2M→64.415%.

  • (a) k tối ưu là thật — đường cong hình chữ V: quét k từ 1 tới 15 trên cùng một Bloom 10 bit/phần tử, FP giảm dần từ 9.58% (k=1) xuống đáy 0.814% ở k=7, rồi tăng lại lên 2.27% (k=15). Đúng như lý thuyết dự đoán: k tối ưu ≈ (m/n)·ln2 = 6.9. Ít hàm băm quá thì phân biệt kém; nhiều quá thì làm mảng bit đầy — cả hai đều tăng báo nhầm. Và FP thực khớp FP lý thuyết ở mọi giá trị k.
  • (b) Công thức m cho đúng số bit cần: cho trước p mục tiêu, m = -n·ln(p)/(ln2)² tính ra số bit, và đo thực tế FP đạt đúng mục tiêu: p=1% cần 9.6 bit/phần tử (FP thực 1.012%), p=0.1% cần 14.4 bit (0.102%), p=0.01% cần 19.2 bit (0.007%). Một quy tắc đẹp lộ ra: mỗi lần giảm p xuống 10 lần, thêm ~4.8 bit/phần tử. Đây là cách bạn dự toán bộ nhớ Bloom chính xác trước khi triển khai.
  • (c) Cái bẫy chết người — nhồi quá n: đây là lỗi thực tế phổ biến nhất. Một Bloom thiết kế cho 500k phần tử (m=5 triệu bit, k=7) cho báo nhầm 0.814% khi dùng đúng. Nhưng nếu bạn nhồi gấp đôi (1 triệu), báo nhầm nhảy lên 13.789% — tăng 17 lần! Nhồi gấp bốn (2 triệu) thì 64.415% — gần như vô dụng. Vì Bloom cố định m bit, thêm nhiều phần tử làm quá nhiều bit bật ("Bloom đầy"), và báo nhầm tăng theo hàm mũ. Đây là lý do phải ước lượng n cho đúng và cấp m dư.

Đánh đổi cần cân nhắc

Ước lượng n cao hơn thực tế — thà thừa bit còn hơn Bloom đầy. Vì báo nhầm bùng lên khi nhồi quá, nên khi không chắc n, hãy ước lượng cao và cấp m theo đó. Dư một chút bit chỉ tốn thêm chút bộ nhớ (tuyến tính), nhưng thiếu bit làm báo nhầm nổ theo hàm mũ. Nếu n thực sự không đoán được (tập tăng trưởng liên tục), dùng scalable Bloom filter — thêm các Bloom mới khi cái cũ đầy, giữ báo nhầm tổng dưới ngưỡng.

k phải là số nguyên — làm tròn từ công thức, và đừng sợ lệch một chút. Công thức cho k = (m/n)·ln2 thường ra số lẻ (6.9), bạn làm tròn thành 7. Đường cong FP quanh đỉnh khá phẳng (k=5, 7, 9 chênh nhau ít — 0.949%, 0.814%, 0.910%), nên lệch k một hai đơn vị không tai hại. Nếu tính toán k hash tốn kém, chọn k nhỏ hơn một chút (ví dụ 5 thay 7) đổi chút báo nhầm lấy tốc độ cũng là đánh đổi hợp lý.

Bit/phần tử là ngôn ngữ chung để nói về Bloom. Khi so sánh hoặc thiết kế, đừng nói về m tuyệt đối (phụ thuộc n) mà nói về bit/phần tử (m/n) — nó quyết định báo nhầm bất kể quy mô. "10 bit/phần tử cho ~1%" là con số đáng nhớ. Các thư viện Bloom thường nhận trực tiếp (n, p) và tự tính m, k — nhưng hiểu quan hệ này giúp bạn biết khi nào Bloom không hợp (ví dụ cần p=0.001% thì 24 bit/phần tử, có khi lưu thẳng còn rẻ hơn).

Ba ý mang về

  1. Có k tối ưu, không phải càng nhiều càng tốt: đo thật quét k=1..15 cho đường cong chữ V, đáy ở k=7 (0.814%) khớp công thức (m/n)·ln2=6.9; ít hàm băm quá phân biệt kém, nhiều quá làm mảng bit đầy — cả hai tăng báo nhầm.
  2. Công thức cho đúng số bit theo p mục tiêu: đo thật m = -n·ln(p)/(ln2)² đạt đúng 1%/0.1%/0.01% với 9.6/14.4/19.2 bit/phần tử — mỗi lần giảm p 10 lần thêm ~4.8 bit; dùng để dự toán bộ nhớ chính xác.
  3. Nhồi quá n là cái bẫy chết người: đo thật Bloom cho 500k khi nhồi 1M làm báo nhầm nhảy 0.814%→13.789% (17 lần), 2M→64.4% — báo nhầm bùng theo hàm mũ; ước lượng n cao, cấp m dư, hoặc dùng scalable Bloom.

Nguồn

Phần sau ta vá một giới hạn của Bloom: không xóa được. Counting Bloom filter thay mỗi bit bằng một bộ đếm nhỏ để hỗ trợ xóa — đo thật cái giá bộ nhớ phải trả cho khả năng đó.