From a8488663c785a9d660d14618817d222aa88b85e0 Mon Sep 17 00:00:00 2001 From: martinsumner Date: Tue, 24 Jan 2017 15:48:12 +0000 Subject: [PATCH 1/9] Alternate tinybloom Previously the code had used a tiny bloom - but this proved to be expensive to build. Looking at the alternative of a slot-size only tiny bloom --- src/leveled_tinybloom.erl | 230 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 src/leveled_tinybloom.erl diff --git a/src/leveled_tinybloom.erl b/src/leveled_tinybloom.erl new file mode 100644 index 0000000..13c7ae0 --- /dev/null +++ b/src/leveled_tinybloom.erl @@ -0,0 +1,230 @@ +%% -------- TinyBloom --------- +%% +%% A fixed size bloom that supports 128 keys only, made to try and minimise +%% the cost of producing the bloom +%% + + +-module(leveled_tinybloom). + +-include("include/leveled.hrl"). + +-define(TWO_POWER, + list_to_tuple( + lists:reverse( + element(2, + lists:foldl( + fun(_I, {AccLast, AccList}) -> + {AccLast * 2, + [(AccLast * 2)|AccList]} + end, + {1, [1]}, + lists:seq(2, 32)) + ) + ) + ) + ). + +-include_lib("eunit/include/eunit.hrl"). + +-export([ + create_bloom/1, + check_hash/2 + ]). + + +%%%============================================================================ +%%% API +%%%============================================================================ + + +create_bloom(HashList) -> + SlotSplit = + case length(HashList) of + L when L > 64 -> + 15; + L when L > 32 -> + 7; + L when L > 16 -> + 3; + _ -> + 1 + end, + add_hashlist(HashList, + array:new([{size, SlotSplit + 1}, {default, 0}]), + SlotSplit). + +check_hash(Hash, BloomBin) -> + SlotSplit = (byte_size(BloomBin) div 4) - 1, + {Slot, H0, H1} = split_hash(Hash, SlotSplit), + Mask = get_mask(H0, H1), + Pos = Slot * 4, + <<_H:Pos/binary, CheckInt:32/integer, _T/binary>> = BloomBin, + case CheckInt band Mask of + Mask -> + true; + _ -> + false + end. + +%%%============================================================================ +%%% Internal Functions +%%%============================================================================ + +split_hash(Hash, SlotSplit) -> + Slot = Hash band SlotSplit, + H0 = (Hash bsr 4) band 31, + H1 = (Hash bsr 9) band 31, + H3 = (Hash bsr 14) band 31, + H4 = (Hash bsr 19) band 31, + {Slot, H0 bxor H3, H1 bxor H4}. + +get_mask(H0, H1) -> + case H0 == H1 of + true -> + element(H0 + 1, ?TWO_POWER); + false -> + element(H0 + 1, ?TWO_POWER) + element(H1 + 1, ?TWO_POWER) + end. + +add_hashlist([], SlotArray, SlotSplit) -> + BuildBinFun = + fun(I, Acc) -> + Bloom = array:get(I, SlotArray), + <> + end, + lists:foldl(BuildBinFun, <<>>, lists:seq(0, SlotSplit)); +add_hashlist([TopHash|T], SlotArray, SlotSplit) -> + {Slot, H0, H1} = split_hash(TopHash, SlotSplit), + Mask = get_mask(H0, H1), + I = array:get(Slot, SlotArray), + add_hashlist(T, array:set(Slot, I bor Mask, SlotArray), SlotSplit). + + +%%%============================================================================ +%%% Test +%%%============================================================================ + +-ifdef(TEST). + +generate_randomkeys(Seqn, Count, BucketRangeLow, BucketRangeHigh) -> + generate_randomkeys(Seqn, + Count, + [], + BucketRangeLow, + BucketRangeHigh). + +generate_randomkeys(_Seqn, 0, Acc, _BucketLow, _BucketHigh) -> + Acc; +generate_randomkeys(Seqn, Count, Acc, BucketLow, BRange) -> + BRand = random:uniform(BRange), + BNumber = string:right(integer_to_list(BucketLow + BRand), 4, $0), + KNumber = string:right(integer_to_list(random:uniform(10000)), 6, $0), + LedgerKey = leveled_codec:to_ledgerkey("Bucket" ++ BNumber, + "Key" ++ KNumber, + o), + {_B, _K, KV, _H} = leveled_codec:generate_ledgerkv(LedgerKey, + Seqn, + crypto:rand_bytes(64), + 64, + infinity), + generate_randomkeys(Seqn + 1, + Count - 1, + [KV|Acc], + BucketLow, + BRange). + + +get_hashlist(N) -> + KVL0 = lists:ukeysort(1, generate_randomkeys(1, N * 2, 1, 20)), + KVL = lists:sublist(KVL0, N), + HashFun = + fun({K, _V}) -> + leveled_codec:magic_hash(K) + end, + lists:map(HashFun, KVL). + +check_all_hashes(BloomBin, HashList) -> + CheckFun = + fun(Hash) -> + ?assertMatch(true, check_hash(Hash, BloomBin)) + end, + lists:foreach(CheckFun, HashList). + +check_neg_hashes(BloomBin, HashList, Counters) -> + CheckFun = + fun(Hash, {AccT, AccF}) -> + case check_hash(Hash, BloomBin) of + true -> + {AccT + 1, AccF}; + false -> + {AccT, AccF + 1} + end + end, + lists:foldl(CheckFun, Counters, HashList). + +bloom_test() -> + test_bloom(128), + test_bloom(64), + test_bloom(32). + + +test_bloom(N) -> + HashList1 = get_hashlist(N), + HashList2 = get_hashlist(N), + HashList3 = get_hashlist(N), + HashList4 = get_hashlist(N), + + SWa = os:timestamp(), + BloomBin1 = create_bloom(HashList1), + BloomBin2 = create_bloom(HashList2), + BloomBin3 = create_bloom(HashList3), + BloomBin4 = create_bloom(HashList4), + TSa = timer:now_diff(os:timestamp(), SWa), + + case N of + 128 -> + ?assertMatch(64, byte_size(BloomBin1)), + ?assertMatch(64, byte_size(BloomBin2)), + ?assertMatch(64, byte_size(BloomBin3)), + ?assertMatch(64, byte_size(BloomBin4)); + _ -> + ok + end, + + SWb = os:timestamp(), + check_all_hashes(BloomBin1, HashList1), + check_all_hashes(BloomBin2, HashList2), + check_all_hashes(BloomBin3, HashList3), + check_all_hashes(BloomBin4, HashList4), + TSb = timer:now_diff(os:timestamp(), SWb), + + HashPool = get_hashlist(N * 2), + HashListOut1 = lists:sublist(lists:subtract(HashPool, HashList1), N), + HashListOut2 = lists:sublist(lists:subtract(HashPool, HashList2), N), + HashListOut3 = lists:sublist(lists:subtract(HashPool, HashList3), N), + HashListOut4 = lists:sublist(lists:subtract(HashPool, HashList4), N), + + SWc = os:timestamp(), + C0 = {0, 0}, + C1 = check_neg_hashes(BloomBin1, HashListOut1, C0), + C2 = check_neg_hashes(BloomBin2, HashListOut2, C1), + C3 = check_neg_hashes(BloomBin3, HashListOut3, C2), + C4 = check_neg_hashes(BloomBin4, HashListOut4, C3), + {Pos, Neg} = C4, + FPR = Pos / (Pos + Neg), + TSc = timer:now_diff(os:timestamp(), SWc), + + io:format(user, + "Test with size ~w has microsecond timings: -" + ++ " build ~w check ~w neg_check ~w and fpr ~w~n", + [N, TSa, TSb, TSc, FPR]). + +twopower_test() -> + ?assertMatch(1, element(1, ?TWO_POWER)), + ?assertMatch(128, element(8, ?TWO_POWER)), + ?assertMatch(2147483648, element(32, ?TWO_POWER)). + + + +-endif. \ No newline at end of file From 93448b76ba2ae7f9901335515dcc990474ad217e Mon Sep 17 00:00:00 2001 From: martinsumner Date: Tue, 24 Jan 2017 15:49:19 +0000 Subject: [PATCH 2/9] Add some extra length checks --- src/leveled_tinybloom.erl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/leveled_tinybloom.erl b/src/leveled_tinybloom.erl index 13c7ae0..2d1a0cb 100644 --- a/src/leveled_tinybloom.erl +++ b/src/leveled_tinybloom.erl @@ -188,6 +188,11 @@ test_bloom(N) -> ?assertMatch(64, byte_size(BloomBin2)), ?assertMatch(64, byte_size(BloomBin3)), ?assertMatch(64, byte_size(BloomBin4)); + 64 -> + ?assertMatch(32, byte_size(BloomBin1)), + ?assertMatch(32, byte_size(BloomBin2)), + ?assertMatch(32, byte_size(BloomBin3)), + ?assertMatch(32, byte_size(BloomBin4)); _ -> ok end, From 9d95057518e66b308eaa70da625067009eb1e690 Mon Sep 17 00:00:00 2001 From: martinsumner Date: Tue, 24 Jan 2017 17:00:30 +0000 Subject: [PATCH 3/9] Do it daft way - will it be faster? --- src/leveled_tinybloom.erl | 112 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/src/leveled_tinybloom.erl b/src/leveled_tinybloom.erl index 2d1a0cb..681bf17 100644 --- a/src/leveled_tinybloom.erl +++ b/src/leveled_tinybloom.erl @@ -50,9 +50,16 @@ create_bloom(HashList) -> _ -> 1 end, - add_hashlist(HashList, - array:new([{size, SlotSplit + 1}, {default, 0}]), - SlotSplit). + case SlotSplit of + 15 -> + add_hashlist(HashList, + SlotSplit, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + _ -> + add_hashlist(HashList, + array:new([{size, SlotSplit + 1}, {default, 0}]), + SlotSplit) + end. check_hash(Hash, BloomBin) -> SlotSplit = (byte_size(BloomBin) div 4) - 1, @@ -100,6 +107,101 @@ add_hashlist([TopHash|T], SlotArray, SlotSplit) -> I = array:get(Slot, SlotArray), add_hashlist(T, array:set(Slot, I bor Mask, SlotArray), SlotSplit). +add_hashlist([], _S, S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF) -> + <>; +add_hashlist([TopHash|T], + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF) -> + {Slot, H0, H1} = split_hash(TopHash, SlotSplit), + Mask = get_mask(H0, H1), + case Slot of + 0 -> + add_hashlist(T, + SlotSplit, + S0 bor Mask, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF); + 1 -> + add_hashlist(T, + SlotSplit, + S0, S1 bor Mask, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF); + 2 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2 bor Mask, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF); + 3 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3 bor Mask, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF); + 4 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4 bor Mask, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF); + 5 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5 bor Mask, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF); + 6 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6 bor Mask, S7, S8, S9, + SA, SB, SC, SD, SE, SF); + 7 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7 bor Mask, S8, S9, + SA, SB, SC, SD, SE, SF); + 8 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8 bor Mask, S9, + SA, SB, SC, SD, SE, SF); + 9 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9 bor Mask, + SA, SB, SC, SD, SE, SF); + 10 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA bor Mask, SB, SC, SD, SE, SF); + 11 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB bor Mask, SC, SD, SE, SF); + 12 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC bor Mask, SD, SE, SF); + 13 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD bor Mask, SE, SF); + 14 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE bor Mask, SF); + 15 -> + add_hashlist(T, + SlotSplit, + S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, + SA, SB, SC, SD, SE, SF bor Mask) + end. + %%%============================================================================ %%% Test @@ -166,7 +268,9 @@ check_neg_hashes(BloomBin, HashList, Counters) -> bloom_test() -> test_bloom(128), test_bloom(64), - test_bloom(32). + test_bloom(32), + test_bloom(16), + test_bloom(8). test_bloom(N) -> From 8c3d0fc49343686bbc4ae04bb13ee32eae5d26fd Mon Sep 17 00:00:00 2001 From: martinsumner Date: Tue, 24 Jan 2017 17:15:39 +0000 Subject: [PATCH 4/9] Alternate implementation --- src/leveled_tinybloom.erl | 59 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/leveled_tinybloom.erl b/src/leveled_tinybloom.erl index 681bf17..3219eaf 100644 --- a/src/leveled_tinybloom.erl +++ b/src/leveled_tinybloom.erl @@ -39,28 +39,26 @@ create_bloom(HashList) -> - SlotSplit = - case length(HashList) of - L when L > 64 -> - 15; - L when L > 32 -> - 7; - L when L > 16 -> - 3; - _ -> - 1 - end, - case SlotSplit of - 15 -> + case length(HashList) of + 0 -> + <<>>; + L when L > 32 -> add_hashlist(HashList, - SlotSplit, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - _ -> + 15, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0); + L when L > 16 -> add_hashlist(HashList, - array:new([{size, SlotSplit + 1}, {default, 0}]), - SlotSplit) + array:new([{size, 4}, {default, 0}]), + 3); + _ -> + add_hashlist(HashList, + array:new([{size, 2}, {default, 0}]), + 1) end. +check_hash(_Hash, <<>>) -> + false; check_hash(Hash, BloomBin) -> SlotSplit = (byte_size(BloomBin) div 4) - 1, {Slot, H0, H1} = split_hash(Hash, SlotSplit), @@ -265,6 +263,12 @@ check_neg_hashes(BloomBin, HashList, Counters) -> end, lists:foldl(CheckFun, Counters, HashList). + +empty_bloom_test() -> + BloomBin0 = create_bloom([]), + ?assertMatch({0, 4}, + check_neg_hashes(BloomBin0, [0, 10, 100, 100000], {0, 0})). + bloom_test() -> test_bloom(128), test_bloom(64), @@ -272,7 +276,6 @@ bloom_test() -> test_bloom(16), test_bloom(8). - test_bloom(N) -> HashList1 = get_hashlist(N), HashList2 = get_hashlist(N), @@ -288,17 +291,15 @@ test_bloom(N) -> case N of 128 -> - ?assertMatch(64, byte_size(BloomBin1)), - ?assertMatch(64, byte_size(BloomBin2)), - ?assertMatch(64, byte_size(BloomBin3)), - ?assertMatch(64, byte_size(BloomBin4)); + ?assertMatch(64, byte_size(BloomBin1)); 64 -> - ?assertMatch(32, byte_size(BloomBin1)), - ?assertMatch(32, byte_size(BloomBin2)), - ?assertMatch(32, byte_size(BloomBin3)), - ?assertMatch(32, byte_size(BloomBin4)); - _ -> - ok + ?assertMatch(64, byte_size(BloomBin1)); + 32 -> + ?assertMatch(16, byte_size(BloomBin1)); + 16 -> + ?assertMatch(8, byte_size(BloomBin1)); + 8 -> + ?assertMatch(8, byte_size(BloomBin1)) end, SWb = os:timestamp(), From f8f2e02d9295835b2491602aaa285f3ba370e29a Mon Sep 17 00:00:00 2001 From: Martin Sumner Date: Tue, 24 Jan 2017 18:09:51 +0000 Subject: [PATCH 5/9] Use bel for powers of two MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D’oh. That’s much much faster of course! --- src/leveled_tinybloom.erl | 73 ++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/src/leveled_tinybloom.erl b/src/leveled_tinybloom.erl index 3219eaf..f7a59ca 100644 --- a/src/leveled_tinybloom.erl +++ b/src/leveled_tinybloom.erl @@ -9,22 +9,6 @@ -include("include/leveled.hrl"). --define(TWO_POWER, - list_to_tuple( - lists:reverse( - element(2, - lists:foldl( - fun(_I, {AccLast, AccList}) -> - {AccLast * 2, - [(AccLast * 2)|AccList]} - end, - {1, [1]}, - lists:seq(2, 32)) - ) - ) - ) - ). - -include_lib("eunit/include/eunit.hrl"). -export([ @@ -48,13 +32,9 @@ create_bloom(HashList) -> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); L when L > 16 -> - add_hashlist(HashList, - array:new([{size, 4}, {default, 0}]), - 3); + add_hashlist(HashList, 3, 0, 0, 0, 0); _ -> - add_hashlist(HashList, - array:new([{size, 2}, {default, 0}]), - 1) + add_hashlist(HashList, 1, 0, 0) end. check_hash(_Hash, <<>>) -> @@ -87,23 +67,43 @@ split_hash(Hash, SlotSplit) -> get_mask(H0, H1) -> case H0 == H1 of true -> - element(H0 + 1, ?TWO_POWER); + 1 bsl H0; false -> - element(H0 + 1, ?TWO_POWER) + element(H1 + 1, ?TWO_POWER) + (1 bsl H0) + (1 bsl H1) end. -add_hashlist([], SlotArray, SlotSplit) -> - BuildBinFun = - fun(I, Acc) -> - Bloom = array:get(I, SlotArray), - <> - end, - lists:foldl(BuildBinFun, <<>>, lists:seq(0, SlotSplit)); -add_hashlist([TopHash|T], SlotArray, SlotSplit) -> + +%% This looks ugly and clunky, but in tests it was quicker than modifying an +%% Erlang term like an array as it is passed around the loop + +add_hashlist([], _S, S0, S1) -> + <>; +add_hashlist([TopHash|T], SlotSplit, S0, S1) -> + {Slot, H0, H1} = split_hash(TopHash, SlotSplit), + SW = os:timestamp(), + Mask = get_mask(H0, H1), + case Slot of + 0 -> + add_hashlist(T, SlotSplit, S0 bor Mask, S1); + 1 -> + add_hashlist(T, SlotSplit, S0, S1 bor Mask) + end. + +add_hashlist([], _S, S0, S1, S2, S3) -> + <>; +add_hashlist([TopHash|T], SlotSplit, S0, S1, S2, S3) -> {Slot, H0, H1} = split_hash(TopHash, SlotSplit), Mask = get_mask(H0, H1), - I = array:get(Slot, SlotArray), - add_hashlist(T, array:set(Slot, I bor Mask, SlotArray), SlotSplit). + case Slot of + 0 -> + add_hashlist(T, SlotSplit, S0 bor Mask, S1, S2, S3); + 1 -> + add_hashlist(T, SlotSplit, S0, S1 bor Mask, S2, S3); + 2 -> + add_hashlist(T, SlotSplit, S0, S1, S2 bor Mask, S3); + 3 -> + add_hashlist(T, SlotSplit, S0, S1, S2, S3 bor Mask) + end. add_hashlist([], _S, S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, SA, SB, SC, SD, SE, SF) -> @@ -330,11 +330,6 @@ test_bloom(N) -> ++ " build ~w check ~w neg_check ~w and fpr ~w~n", [N, TSa, TSb, TSc, FPR]). -twopower_test() -> - ?assertMatch(1, element(1, ?TWO_POWER)), - ?assertMatch(128, element(8, ?TWO_POWER)), - ?assertMatch(2147483648, element(32, ?TWO_POWER)). - -endif. \ No newline at end of file From d57b74d967f7e559e3f1c89447c476945d0aee0e Mon Sep 17 00:00:00 2001 From: martinsumner Date: Tue, 24 Jan 2017 21:51:12 +0000 Subject: [PATCH 6/9] Re-introduce tinybloom to SST This had been removed due to the CPU cost of adding - however then the tinybloom wa simplemented by directly manipulating bits through binary comprehension - rather than applying bor band bsl bsr operations. With these operations the cost of producing and checking the bloom is <10% by comparison. --- src/leveled_log.erl | 2 +- src/leveled_sst.erl | 144 ++++++++++++++++++++------------------------ 2 files changed, 67 insertions(+), 79 deletions(-) diff --git a/src/leveled_log.erl b/src/leveled_log.erl index c736fe9..26b15bf 100644 --- a/src/leveled_log.erl +++ b/src/leveled_log.erl @@ -451,7 +451,7 @@ sst_timing({N, SSTTimerD}, SW, TimerType) -> end. sst_keylist() -> - [slot_bloom, slot_fetch]. + [tiny_bloom, slot_bloom, slot_fetch]. get_timing(undefined, SW, TimerType) -> diff --git a/src/leveled_sst.erl b/src/leveled_sst.erl index 58573a8..7978c63 100644 --- a/src/leveled_sst.erl +++ b/src/leveled_sst.erl @@ -114,7 +114,8 @@ -record(slot_index_value, {slot_id :: integer(), start_position :: integer(), - length :: integer()}). + length :: integer(), + bloom :: binary()}). -record(summary, {first_key :: tuple(), last_key :: tuple(), @@ -398,47 +399,51 @@ fetch(LedgerKey, Hash, State) -> Summary = State#state.summary, Slot = lookup_slot(LedgerKey, Summary#summary.index), SlotID = Slot#slot_index_value.slot_id, - CachedBlockIdx = array:get(SlotID - 1, - State#state.blockindex_cache), - case CachedBlockIdx of - none -> - SlotBin = read_slot(State#state.handle, Slot), - {Result, BlockIdx} = binaryslot_get(SlotBin, - LedgerKey, - Hash, - none), - BlockIndexCache = array:set(SlotID - 1, - BlockIdx, + Bloom = Slot#slot_index_value.bloom, + case leveled_tinybloom:check_hash(Hash, Bloom) of + false -> + {not_present, tiny_bloom, SlotID, State}; + true -> + CachedBlockIdx = array:get(SlotID - 1, State#state.blockindex_cache), - {Result, - slot_fetch, - Slot#slot_index_value.slot_id, - State#state{blockindex_cache = BlockIndexCache}}; - _ -> - PosList = find_pos(CachedBlockIdx, - double_hash(Hash, LedgerKey), - [], - 0), - case PosList of - [] -> - {not_present, slot_bloom, SlotID, State}; - _ -> + case CachedBlockIdx of + none -> SlotBin = read_slot(State#state.handle, Slot), - Result = binaryslot_get(SlotBin, - LedgerKey, - Hash, - {true, PosList}), - {element(1, Result), slot_fetch, SlotID, State} - end + {Result, BlockIdx} = binaryslot_get(SlotBin, + LedgerKey, + Hash, + none), + BlockIndexCache = array:set(SlotID - 1, + BlockIdx, + State#state.blockindex_cache), + {Result, + slot_fetch, + Slot#slot_index_value.slot_id, + State#state{blockindex_cache = BlockIndexCache}}; + _ -> + PosList = find_pos(CachedBlockIdx, + double_hash(Hash, LedgerKey), + [], + 0), + case PosList of + [] -> + {not_present, slot_bloom, SlotID, State}; + _ -> + SlotBin = read_slot(State#state.handle, Slot), + Result = binaryslot_get(SlotBin, + LedgerKey, + Hash, + {true, PosList}), + {element(1, Result), slot_fetch, SlotID, State} + end + end end. fetch_range(StartKey, EndKey, ScanWidth, State) -> Summary = State#state.summary, Handle = State#state.handle, - {Slots, LTrim, RTrim} = lookup_slots(StartKey, - EndKey, - Summary#summary.index), + {Slots, RTrim} = lookup_slots(StartKey, EndKey, Summary#summary.index), Self = self(), SL = length(Slots), ExpandedSlots = @@ -447,15 +452,11 @@ fetch_range(StartKey, EndKey, ScanWidth, State) -> []; 1 -> [Slot] = Slots, - case {LTrim, RTrim} of - {true, true} -> + case RTrim of + true -> [{pointer, Self, Slot, StartKey, EndKey}]; - {true, false} -> - [{pointer, Self, Slot, StartKey, all}]; - {false, true} -> - [{pointer, Self, Slot, all, EndKey}]; - {false, false} -> - [{pointer, Self, Slot, all, all}] + false -> + [{pointer, Self, Slot, StartKey, all}] end; N -> {LSlot, MidSlots, RSlot} = @@ -472,21 +473,13 @@ fetch_range(StartKey, EndKey, ScanWidth, State) -> {pointer, Self, S, all, all} end, MidSlots), - case {LTrim, RTrim} of - {true, true} -> + case RTrim of + true -> [{pointer, Self, LSlot, StartKey, all}] ++ MidSlotPointers ++ [{pointer, Self, RSlot, all, EndKey}]; - {true, false} -> + false -> [{pointer, Self, LSlot, StartKey, all}] ++ - MidSlotPointers ++ - [{pointer, Self, RSlot, all, all}]; - {false, true} -> - [{pointer, Self, LSlot, all, all}] ++ - MidSlotPointers ++ - [{pointer, Self, RSlot, all, EndKey}]; - {false, false} -> - [{pointer, Self, LSlot, all, all}] ++ MidSlotPointers ++ [{pointer, Self, RSlot, all, all}] end @@ -603,11 +596,13 @@ build_all_slots(KVL, SC, Pos, SlotID, SlotIdx, BlockIdxA, SlotsBin) -> lists:split(?SLOT_SIZE, KVL) end, {LastKey, _V} = lists:last(SlotList), - {BlockIndex, SlotBin} = generate_binary_slot(SlotList), + {BlockIndex, SlotBin, HashList} = generate_binary_slot(SlotList), Length = byte_size(SlotBin), + Bloom = leveled_tinybloom:create_bloom(HashList), SlotIndexV = #slot_index_value{slot_id = SlotID, start_position = Pos, - length = Length}, + length = Length, + bloom = Bloom}, build_all_slots(KVRem, SC - 1, Pos + Length, @@ -706,9 +701,9 @@ lookup_slots(StartKey, EndKey, Tree) -> {EK, _EndSlot} = lists:last(SlotList), case EK of EndKey -> - {lists:map(MapFun, SlotList), true, false}; + {lists:map(MapFun, SlotList), false}; _ -> - {lists:map(MapFun, SlotList), true, true} + {lists:map(MapFun, SlotList), true} end. @@ -739,7 +734,7 @@ lookup_slots(StartKey, EndKey, Tree) -> generate_binary_slot(KVL) -> HashFoldFun = - fun({K, V}, {PosBinAcc, NoHashCount}) -> + fun({K, V}, {PosBinAcc, NoHashCount, HashAcc}) -> {_SQN, H1} = leveled_codec:strip_to_seqnhashonly({K, V}), case is_integer(H1) of @@ -750,7 +745,8 @@ generate_binary_slot(KVL) -> {<<1:1/integer, PosH1:15/integer, PosBinAcc/binary>>, - 0}; + 0, + [H1|HashAcc]}; N -> % The No Hash Count is an integer between 0 and 127 % and so at read time should count NHC + 1 @@ -760,15 +756,16 @@ generate_binary_slot(KVL) -> 0:1/integer, NHC:7/integer, PosBinAcc/binary>>, - 0} + 0, + HashAcc} end; false -> - {PosBinAcc, NoHashCount + 1} + {PosBinAcc, NoHashCount + 1, HashAcc} end end, - {PosBinIndex0, NHC} = lists:foldr(HashFoldFun, {<<>>, 0}, KVL), + {PosBinIndex0, NHC, HashL} = lists:foldr(HashFoldFun, {<<>>, 0, []}, KVL), PosBinIndex1 = case NHC of 0 -> @@ -825,7 +822,7 @@ generate_binary_slot(KVL) -> CRC32 = erlang:crc32(SlotBin), FullBin = <>, - {PosBinIndex1, FullBin}. + {PosBinIndex1, FullBin, HashL}. binaryslot_get(FullBin, Key, Hash, CachedPosLookup) -> @@ -1212,18 +1209,9 @@ indexed_list_test() -> KVL0 = lists:ukeysort(1, generate_randomkeys(1, N, 1, 4)), KVL1 = lists:sublist(KVL0, 128), - % BloomAddFun = - % fun({H, K}, {Bloom, Total, Max}) -> - % SW = os:timestamp(), - % Bloom0 = leveled_tinybloom:tiny_enter(H, K, Bloom), - % T0 = timer:now_diff(os:timestamp(), SW), - % {Bloom0, Total + T0, max(T0, Max)} - - % end, - SW0 = os:timestamp(), - {_PosBinIndex1, FullBin} = generate_binary_slot(KVL1), + {_PosBinIndex1, FullBin, _HL} = generate_binary_slot(KVL1), io:format(user, "Indexed list created slot in ~w microseconds of size ~w~n", [timer:now_diff(os:timestamp(), SW0), byte_size(FullBin)]), @@ -1251,7 +1239,7 @@ indexed_list_mixedkeys_test() -> KVL1 = lists:sublist(KVL0, 33), Keys = lists:ukeysort(1, generate_indexkeys(60) ++ KVL1), - {_PosBinIndex1, FullBin} = generate_binary_slot(Keys), + {_PosBinIndex1, FullBin, _HL} = generate_binary_slot(Keys), {TestK1, TestV1} = lists:nth(4, KVL1), MH1 = leveled_codec:magic_hash(TestK1), @@ -1277,7 +1265,7 @@ indexed_list_mixedkeys2_test() -> IdxKeys2 = lists:ukeysort(1, generate_indexkeys(30)), % this isn't actually ordered correctly Keys = IdxKeys1 ++ KVL1 ++ IdxKeys2, - {_PosBinIndex1, FullBin} = generate_binary_slot(Keys), + {_PosBinIndex1, FullBin, _HL} = generate_binary_slot(Keys), lists:foreach(fun({K, V}) -> MH = leveled_codec:magic_hash(K), test_binary_slot(FullBin, K, MH, {K, V}) @@ -1286,7 +1274,7 @@ indexed_list_mixedkeys2_test() -> indexed_list_allindexkeys_test() -> Keys = lists:sublist(lists:ukeysort(1, generate_indexkeys(150)), 128), - {PosBinIndex1, FullBin} = generate_binary_slot(Keys), + {PosBinIndex1, FullBin, _HL} = generate_binary_slot(Keys), ?assertMatch(<<127:8/integer>>, PosBinIndex1), % SW = os:timestamp(), BinToList = binaryslot_tolist(FullBin), @@ -1299,7 +1287,7 @@ indexed_list_allindexkeys_test() -> indexed_list_allindexkeys_trimmed_test() -> Keys = lists:sublist(lists:ukeysort(1, generate_indexkeys(150)), 128), - {PosBinIndex1, FullBin} = generate_binary_slot(Keys), + {PosBinIndex1, FullBin, _HL} = generate_binary_slot(Keys), ?assertMatch(<<127:8/integer>>, PosBinIndex1), ?assertMatch(Keys, binaryslot_trimmedlist(FullBin, {i, @@ -1337,7 +1325,7 @@ indexed_list_mixedkeys_bitflip_test() -> KVL0 = lists:ukeysort(1, generate_randomkeys(1, 50, 1, 4)), KVL1 = lists:sublist(KVL0, 33), Keys = lists:ukeysort(1, generate_indexkeys(60) ++ KVL1), - {_PosBinIndex1, FullBin} = generate_binary_slot(Keys), + {_PosBinIndex1, FullBin, _HL} = generate_binary_slot(Keys), L = byte_size(FullBin), Byte1 = random:uniform(L), <> = FullBin, From 10a64b86be19bb901107a7d60e5742f30f79e7f1 Mon Sep 17 00:00:00 2001 From: martinsumner Date: Tue, 24 Jan 2017 21:53:43 +0000 Subject: [PATCH 7/9] Remove rogue timestamp --- src/leveled_tinybloom.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/leveled_tinybloom.erl b/src/leveled_tinybloom.erl index f7a59ca..0126f67 100644 --- a/src/leveled_tinybloom.erl +++ b/src/leveled_tinybloom.erl @@ -80,7 +80,6 @@ add_hashlist([], _S, S0, S1) -> <>; add_hashlist([TopHash|T], SlotSplit, S0, S1) -> {Slot, H0, H1} = split_hash(TopHash, SlotSplit), - SW = os:timestamp(), Mask = get_mask(H0, H1), case Slot of 0 -> From 684f1bcd993d536df83291e93330f4ee9e39a032 Mon Sep 17 00:00:00 2001 From: martinsumner Date: Wed, 25 Jan 2017 01:07:15 +0000 Subject: [PATCH 8/9] Increase bloom size --- src/leveled_tinybloom.erl | 55 ++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/leveled_tinybloom.erl b/src/leveled_tinybloom.erl index 0126f67..23ff343 100644 --- a/src/leveled_tinybloom.erl +++ b/src/leveled_tinybloom.erl @@ -16,6 +16,10 @@ check_hash/2 ]). +-define(BITS_PER_KEY, 8). % Must be 8 or 4 +-define(INTEGER_SIZE, ?BITS_PER_KEY * 8). +-define(BAND_MASK, ?INTEGER_SIZE - 1). + %%%============================================================================ %%% API @@ -40,11 +44,12 @@ create_bloom(HashList) -> check_hash(_Hash, <<>>) -> false; check_hash(Hash, BloomBin) -> - SlotSplit = (byte_size(BloomBin) div 4) - 1, + SlotSplit = (byte_size(BloomBin) div ?BITS_PER_KEY) - 1, {Slot, H0, H1} = split_hash(Hash, SlotSplit), Mask = get_mask(H0, H1), - Pos = Slot * 4, - <<_H:Pos/binary, CheckInt:32/integer, _T/binary>> = BloomBin, + Pos = Slot * ?BITS_PER_KEY, + IntSize = ?INTEGER_SIZE, + <<_H:Pos/binary, CheckInt:IntSize/integer, _T/binary>> = BloomBin, case CheckInt band Mask of Mask -> true; @@ -58,11 +63,12 @@ check_hash(Hash, BloomBin) -> split_hash(Hash, SlotSplit) -> Slot = Hash band SlotSplit, - H0 = (Hash bsr 4) band 31, - H1 = (Hash bsr 9) band 31, - H3 = (Hash bsr 14) band 31, - H4 = (Hash bsr 19) band 31, - {Slot, H0 bxor H3, H1 bxor H4}. + H0 = (Hash bsr 4) band (?BAND_MASK), + H1 = (Hash bsr 10) band (?BAND_MASK), + H3 = (Hash bsr 16) band (?BAND_MASK), + H4 = (Hash bsr 22) band (?BAND_MASK), + Slot0 = (Hash bsr 28) band SlotSplit, + {Slot bxor Slot0, H0 bxor H3, H1 bxor H4}. get_mask(H0, H1) -> case H0 == H1 of @@ -77,7 +83,8 @@ get_mask(H0, H1) -> %% Erlang term like an array as it is passed around the loop add_hashlist([], _S, S0, S1) -> - <>; + IntSize = ?INTEGER_SIZE, + <>; add_hashlist([TopHash|T], SlotSplit, S0, S1) -> {Slot, H0, H1} = split_hash(TopHash, SlotSplit), Mask = get_mask(H0, H1), @@ -89,7 +96,9 @@ add_hashlist([TopHash|T], SlotSplit, S0, S1) -> end. add_hashlist([], _S, S0, S1, S2, S3) -> - <>; + IntSize = ?INTEGER_SIZE, + <>; add_hashlist([TopHash|T], SlotSplit, S0, S1, S2, S3) -> {Slot, H0, H1} = split_hash(TopHash, SlotSplit), Mask = get_mask(H0, H1), @@ -106,10 +115,15 @@ add_hashlist([TopHash|T], SlotSplit, S0, S1, S2, S3) -> add_hashlist([], _S, S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, SA, SB, SC, SD, SE, SF) -> - <>; + IntSize = ?INTEGER_SIZE, + <>; add_hashlist([TopHash|T], SlotSplit, S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, @@ -288,19 +302,6 @@ test_bloom(N) -> BloomBin4 = create_bloom(HashList4), TSa = timer:now_diff(os:timestamp(), SWa), - case N of - 128 -> - ?assertMatch(64, byte_size(BloomBin1)); - 64 -> - ?assertMatch(64, byte_size(BloomBin1)); - 32 -> - ?assertMatch(16, byte_size(BloomBin1)); - 16 -> - ?assertMatch(8, byte_size(BloomBin1)); - 8 -> - ?assertMatch(8, byte_size(BloomBin1)) - end, - SWb = os:timestamp(), check_all_hashes(BloomBin1, HashList1), check_all_hashes(BloomBin2, HashList2), From 7320b346815b354fc3f84611995163de463ed1d0 Mon Sep 17 00:00:00 2001 From: martinsumner Date: Wed, 25 Jan 2017 12:38:33 +0000 Subject: [PATCH 9/9] Comment update --- src/leveled_sst.erl | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/leveled_sst.erl b/src/leveled_sst.erl index 7978c63..c8600bb 100644 --- a/src/leveled_sst.erl +++ b/src/leveled_sst.erl @@ -8,7 +8,7 @@ %% -------- Slots --------- %% %% The view is built from sublists referred to as slot. Each slot is up to 128 -%% keys and values in size. Three strategis have been benchmarked for the +%% keys and values in size. Three strategies have been benchmarked for the %% slot: a skiplist, a gb-tree, four blocks of flat lists with an index. %% %% Skiplist: @@ -23,7 +23,7 @@ %% %% Indexed Blocks: %% build and serialise slot 342 microseconds -%% de-deriaise and check * 128 - 6746 microseconds +%% de-deserialise and check * 128 - 6746 microseconds %% flatten back to list - 187 microseconds %% %% The negative side of using Indexed Blocks is the storage of the index. In @@ -34,23 +34,19 @@ %% %% -------- Blooms --------- %% -%% There is a summary bloom for the table. the summary bloom is split by the -%% first byte of the hash, and consists of two hashes (derived from the -%% remainder of the hash). This is the top bloom, and the size varies by -%% level. -%% Level 0 has 8 bits per key - 0.05 fpr -%% Level 1 has 6 bits per key - 0.08 fpr -%% Other Levels have 4 bits per key - 0.15 fpr +%% There is a bloom for each slot - based on two hashes and 8 bits per key. %% -%% With the indexed block implementation of the slot a second slot-level bloom -%% is unnecessary (as the index itself yields a 0.003 % fpr). +%% Hashing for blooms is a challenge, as the slot is a slice of an ordered +%% list of keys with a fixed format. It is likely that the keys may vary by +%% only one or two ascii characters, and there is a desire to avoid the +%% overhead of cryptographic hash functions that may be able to handle this. %% %% -------- Summary --------- %% %% Each file has a summary - which is the 128 keys at the top of each slot in %% a skiplist, with some basic metadata about the slot stored as the value. %% -%% The summary is stored seperately to the slots (wihtin the same file). +%% The summary is stored seperately to the slots (within the same file). %% %% -------- CRC Checks --------- %%