Skip to content

Commit d3bb4e9

Browse files
nigorolllemire
andauthored
Add pack and unpack of a more compact serialization format (#68)
* Store the original size argument in struct binary_fuse{8,16}_s ... in preparation of a more compact serialization format: All other parameters except for the Seed are derived from the size parameter. The drawback is that this format is sensitive to changes of binary_fuse8_allocate(). Due to alignment, this does not need any more space on 64bit. (There were 5 32bit values inbetween two 64bit values) Yet formally, this is a breaking change of the in-core format, which should not be used to store information across versions. See follow up commits for new compact serialization formats. * Add {xor,binary_fuse}{8,16}_{pack,unpack} serialization formats. Rationale: As mentioned in the previous commit, for binary_fuse filters, we do not need to save values derived from the size, saving 5 x sizeof(uint32_t). For both filter implementations, we add a bitmap to indicate non-zero fingerprint values. This adds 1/{8,16} of the fingerprint array size, but saves one or two bytes for each zero fingerprint. The net result is a packed format which can not be compressed further by zlib for the bundled unit tests. Note that this format is incompatible with the existing _serialize() format and, in the case of binary_fuse, sensitive to changes of the derived parameters in _allocate. Interface: We add _pack_bytes() to match _serialization_bytes(). _pack() and _unpack() match _serialize() and _deserialize(). The existing _{de,}serialize() interfaces take a buffer pointer only and thus implicitly assume that the buffer will be of sufficient size. For the new functions, we add a size_t parameter indicating the size of the buffer and check its bounds in the implementation. _pack returns the used size or zero for "does not fit", so when called with a buffer of arbitrary size, the used space or error condition can be determined without an additional call to _pack_bytes(), avoiding duplicate work. Implementation: We add some XOR_bitf_* macros to address words and individual bits of bitfields. The XOR_ser and XOR_deser macros have the otherwise repeated code for bounds checking and the actual serialization. Because the implementations for the 8 and 16 bit words are equal except for the data type, we add macros and create the actual functions by expanding the macros with the possible data types. Alternatives considered: Compared to _{de,}serialize(), the new functions need to copy individual fingerprint words rather than the whole array at once, which is less efficient. Therefor, an implementation using Duff's Device with branchless code was attempted but dismissed because avoiding out-of-bounds access would require an over-allocated buffer. * Adjust unit tests to new _{un,}pack() interface To exercise the new code without too much of a change to the existing unit test, we change the signature of _{un,}serialize_gen() to take an additional (const) size_t argument, which we ignore for _{un,}serialize(). We add to the reported metrics absolute and relative size information for the "in-core" and "wire" format, the latter jointly referencing to _{un,}serialize() and _{un,}pack(). * Document the new _{un,}pack() interface * tuning the wording and adding a spaceusage benchmark * changing the wording. * rewording. * explicit casts --------- Co-authored-by: Daniel Lemire <[email protected]>
1 parent 5539876 commit d3bb4e9

File tree

7 files changed

+498
-18
lines changed

7 files changed

+498
-18
lines changed

README.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,20 @@ about 0.0015%. The type is `binary_fuse16_t` and you may use it with
5454
functions such as `binary_fuse16_allocate`, `binary_fuse16_populate`,
5555
`binary_fuse8_contain` and `binary_fuse8_free`.
5656
57-
You may serialize the data as follows:
57+
For serialization, there is a choice between an unpacked and a packed format.
58+
59+
The unpacked format is roughly of the same size as in-core data, but uses most
60+
efficient memory copy operations.
61+
62+
The packed format avoids storing zero bytes and relies on a bitset to locate them, so it
63+
should be expected to be somewhat slower. The packed format might be smaller or larger.
64+
It might be beneficial when using 16-bit binary fuse filters for users who need to preserve
65+
every bytes, and who do not care about the computational overhead.
66+
When in doubt, prefer the regular (unpacked) format.
67+
68+
The two formats use slightly different APIs.
69+
70+
You may serialize and deserialize in unpacked format as follows:
5871
5972
```C
6073
size_t buffer_size = binary_fuse16_serialization_bytes(&filter);
@@ -65,9 +78,34 @@ You may serialize the data as follows:
6578
free(buffer);
6679
```
6780

68-
The serialization does not handle endianess: it is expected that you will serialize
69-
and deserialize on the little endian systems. (Big endian systems are vanishingly rare.)
81+
This should be the default.
82+
83+
To serialize and deserialize in packed format, use the `_pack_bytes()`,
84+
`_pack()` and `_unpack()` functions. The latter two have an additional `size_t`
85+
argument for the buffer length. `_pack()` can be used with a buffer of arbitrary
86+
size, it returns the used space if serialization fit into the buffer or 0
87+
otherwise. Note that the packed format will be slower and may not save space
88+
although it is likely smaller on disk when using the 16-bit binary fuse filters.
89+
90+
For example:
91+
92+
```C
93+
size_t buffer_size = binary_fuse16_pack_bytes(&filter);
94+
char *buffer = (char*)malloc(buffer_size);
95+
if (binary_fuse16_pack(&filter, buffer, buffer_size) != buffer_size) {
96+
printf("pack failed\n");
97+
free(buffer);
98+
return;
99+
}
100+
binary_fuse16_free(&filter);
101+
if (! binary_fuse16_unpack(&filter, buffer, buffer_size)) {
102+
printf("unpack failed\n");
103+
}
104+
free(buffer);
105+
```
70106
107+
Either serialization does not handle endianess changes: it is expected that you
108+
serialize and deserialize with equal byte order.
71109
72110
## C++ wrapper
73111

benchmarks/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
add_executable(bench bench.c)
22
target_link_libraries(bench PUBLIC xor_singleheader)
3+
4+
add_executable(spaceusage spaceusage.c)
5+
target_link_libraries(spaceusage PUBLIC xor_singleheader)

benchmarks/spaceusage.c

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#include "binaryfusefilter.h"
2+
#include "xorfilter.h"
3+
#include <stdlib.h>
4+
#include <iso646.h>
5+
6+
typedef struct {
7+
size_t standard;
8+
size_t pack;
9+
} sizes;
10+
11+
sizes fuse16(size_t n) {
12+
binary_fuse16_t filter = {0};
13+
if (! binary_fuse16_allocate(n, &filter)) {
14+
printf("allocation failed\n");
15+
return (sizes) {0, 0};
16+
}
17+
uint64_t* big_set = malloc(n * sizeof(uint64_t));
18+
for(size_t i = 0; i < n; i++) {
19+
big_set[i] = i;
20+
}
21+
bool is_ok = binary_fuse16_populate(big_set, n, &filter);
22+
if(! is_ok ) {
23+
printf("populating failed\n");
24+
}
25+
free(big_set);
26+
sizes s = {
27+
.standard = binary_fuse16_serialization_bytes(&filter),
28+
.pack = binary_fuse16_pack_bytes(&filter)
29+
};
30+
binary_fuse16_free(&filter);
31+
return s;
32+
}
33+
34+
sizes fuse8(size_t n) {
35+
binary_fuse8_t filter = {0};
36+
if (! binary_fuse8_allocate(n, &filter)) {
37+
printf("allocation failed\n");
38+
return (sizes) {0, 0};
39+
}
40+
uint64_t* big_set = malloc(n * sizeof(uint64_t));
41+
for(size_t i = 0; i < n; i++) {
42+
big_set[i] = i;
43+
}
44+
bool is_ok = binary_fuse8_populate(big_set, n, &filter);
45+
if(! is_ok ) {
46+
printf("populating failed\n");
47+
}
48+
free(big_set);
49+
sizes s = {
50+
.standard = binary_fuse8_serialization_bytes(&filter),
51+
.pack = binary_fuse8_pack_bytes(&filter)
52+
};
53+
binary_fuse8_free(&filter);
54+
return s;
55+
}
56+
57+
sizes xor16(size_t n) {
58+
xor16_t filter = {0};
59+
if (! xor16_allocate(n, &filter)) {
60+
printf("allocation failed\n");
61+
return (sizes) {0, 0};
62+
}
63+
uint64_t* big_set = malloc(n * sizeof(uint64_t));
64+
for(size_t i = 0; i < n; i++) {
65+
big_set[i] = i;
66+
}
67+
bool is_ok = xor16_populate(big_set, n, &filter);
68+
if(! is_ok ) {
69+
printf("populating failed\n");
70+
}
71+
free(big_set);
72+
sizes s = {
73+
.standard = xor16_serialization_bytes(&filter),
74+
.pack = xor16_pack_bytes(&filter)
75+
};
76+
xor16_free(&filter);
77+
return s;
78+
}
79+
80+
sizes xor8(size_t n) {
81+
xor8_t filter = {0};
82+
if (! xor8_allocate(n, &filter)) {
83+
printf("allocation failed\n");
84+
return (sizes) {0, 0};
85+
}
86+
uint64_t* big_set = malloc(n * sizeof(uint64_t));
87+
for(size_t i = 0; i < n; i++) {
88+
big_set[i] = i;
89+
}
90+
bool is_ok = xor8_populate(big_set, n, &filter);
91+
if(! is_ok ) {
92+
printf("populating failed\n");
93+
}
94+
free(big_set);
95+
sizes s = {
96+
.standard = xor8_serialization_bytes(&filter),
97+
.pack = xor8_pack_bytes(&filter)
98+
};
99+
xor8_free(&filter);
100+
101+
return s;
102+
}
103+
104+
int main() {
105+
for (size_t n = 10; n <= 10000000; n *= 2) {
106+
printf("%-10zu ", n); // Align number to 10 characters wide
107+
sizes f16 = fuse16(n);
108+
sizes f8 = fuse8(n);
109+
sizes x16 = xor16(n);
110+
sizes x8 = xor8(n);
111+
112+
printf("fuse16: %5.2f %5.2f ", (double)f16.standard * 8.0 / n, (double)f16.pack * 8.0 / n);
113+
printf("fuse8: %5.2f %5.2f ", (double)f8.standard * 8.0 / n, (double)f8.pack * 8.0 / n);
114+
printf("xor16: %5.2f %5.2f ", (double)x16.standard * 8.0 / n, (double)x16.pack * 8.0 / n);
115+
printf("xor8: %5.2f %5.2f ", (double)x8.standard * 8.0 / n, (double)x8.pack * 8.0 / n);
116+
printf("\n");
117+
}
118+
return EXIT_SUCCESS;
119+
}

include/binaryfusefilter.h

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ static inline uint64_t binary_fuse_rng_splitmix64(uint64_t *seed) {
6767

6868
typedef struct binary_fuse8_s {
6969
uint64_t Seed;
70+
uint32_t Size;
7071
uint32_t SegmentLength;
7172
uint32_t SegmentLengthMask;
7273
uint32_t SegmentCount;
@@ -222,6 +223,7 @@ static inline double binary_fuse_calculate_size_factor(uint32_t arity,
222223
static inline bool binary_fuse8_allocate(uint32_t size,
223224
binary_fuse8_t *filter) {
224225
uint32_t arity = 3;
226+
filter->Size = size;
225227
filter->SegmentLength = size == 0 ? 4 : binary_fuse_calculate_segment_length(arity, size);
226228
if (filter->SegmentLength > 262144) {
227229
filter->SegmentLength = 262144;
@@ -258,6 +260,7 @@ static inline void binary_fuse8_free(binary_fuse8_t *filter) {
258260
free(filter->Fingerprints);
259261
filter->Fingerprints = NULL;
260262
filter->Seed = 0;
263+
filter->Size = 0;
261264
filter->SegmentLength = 0;
262265
filter->SegmentLengthMask = 0;
263266
filter->SegmentCount = 0;
@@ -459,6 +462,7 @@ static inline bool binary_fuse8_populate(uint64_t *keys, uint32_t size,
459462

460463
typedef struct binary_fuse16_s {
461464
uint64_t Seed;
465+
uint32_t Size;
462466
uint32_t SegmentLength;
463467
uint32_t SegmentLengthMask;
464468
uint32_t SegmentCount;
@@ -512,6 +516,7 @@ static inline bool binary_fuse16_contain(uint64_t key,
512516
static inline bool binary_fuse16_allocate(uint32_t size,
513517
binary_fuse16_t *filter) {
514518
uint32_t arity = 3;
519+
filter->Size = size;
515520
filter->SegmentLength = size == 0 ? 4 : binary_fuse_calculate_segment_length(arity, size);
516521
if (filter->SegmentLength > 262144) {
517522
filter->SegmentLength = 262144;
@@ -548,6 +553,7 @@ static inline void binary_fuse16_free(binary_fuse16_t *filter) {
548553
free(filter->Fingerprints);
549554
filter->Fingerprints = NULL;
550555
filter->Seed = 0;
556+
filter->Size = 0;
551557
filter->SegmentLength = 0;
552558
filter->SegmentLengthMask = 0;
553559
filter->SegmentCount = 0;
@@ -858,4 +864,111 @@ static inline bool binary_fuse8_deserialize(binary_fuse8_t * filter, const char
858864
return true;
859865
}
860866

867+
// minimal bitfield implementation
868+
#define XOR_bitf_w (sizeof(uint8_t) * 8)
869+
#define XOR_bitf_sz(bits) (((bits) + XOR_bitf_w - 1) / XOR_bitf_w)
870+
#define XOR_bitf_word(bit) (bit / XOR_bitf_w)
871+
#define XOR_bitf_bit(bit) ((1U << (bit % XOR_bitf_w)) % 256)
872+
873+
#define XOR_ser(buf, lim, src) do { \
874+
if ((buf) + sizeof src > (lim)) \
875+
return (0); \
876+
memcpy(buf, &src, sizeof src); \
877+
buf += sizeof src; \
878+
} while (0)
879+
880+
#define XOR_deser(dst, buf, lim) do { \
881+
if ((buf) + sizeof dst > (lim)) \
882+
return (false); \
883+
memcpy(&dst, buf, sizeof dst); \
884+
buf += sizeof dst; \
885+
} while (0)
886+
887+
// return required space for binary_fuse{8,16}_pack()
888+
#define XOR_bytesf(fuse) \
889+
static inline size_t binary_ ## fuse ## _pack_bytes(const binary_ ## fuse ## _t *filter) \
890+
{ \
891+
size_t sz = 0; \
892+
sz += sizeof filter->Seed; \
893+
sz += sizeof filter->Size; \
894+
sz += XOR_bitf_sz(filter->ArrayLength); \
895+
for (size_t i = 0; i < filter->ArrayLength; i++) { \
896+
if (filter->Fingerprints[i] == 0) \
897+
continue; \
898+
sz += sizeof filter->Fingerprints[i]; \
899+
} \
900+
return (sz); \
901+
}
902+
903+
// serialize as packed format, return size used or 0 for insufficient space
904+
#define XOR_packf(fuse) \
905+
static inline size_t binary_ ## fuse ## _pack(const binary_ ## fuse ## _t *filter, char *buffer, size_t space) { \
906+
uint8_t *s = (uint8_t *)(void *)buffer; \
907+
uint8_t *buf = s, *e = buf + space; \
908+
\
909+
XOR_ser(buf, e, filter->Seed); \
910+
XOR_ser(buf, e, filter->Size); \
911+
size_t bsz = XOR_bitf_sz(filter->ArrayLength); \
912+
if (buf + bsz > e) \
913+
return (0); \
914+
uint8_t *bitf = buf; \
915+
memset(bitf, 0, bsz); \
916+
buf += bsz; \
917+
\
918+
for (size_t i = 0; i < filter->ArrayLength; i++) { \
919+
if (filter->Fingerprints[i] == 0) \
920+
continue; \
921+
bitf[XOR_bitf_word(i)] |= XOR_bitf_bit(i); \
922+
XOR_ser(buf, e, filter->Fingerprints[i]); \
923+
} \
924+
return ((size_t)(buf - s)); \
925+
}
926+
927+
#define XOR_unpackf(fuse) \
928+
static inline bool binary_ ## fuse ## _unpack(binary_ ## fuse ## _t *filter, const char *buffer, size_t len) \
929+
{ \
930+
const uint8_t *s = (const uint8_t *)(const void *)buffer; \
931+
const uint8_t *buf = s, *e = buf + len; \
932+
bool r; \
933+
\
934+
uint64_t Seed; \
935+
uint32_t Size; \
936+
\
937+
memset(filter, 0, sizeof *filter); \
938+
XOR_deser(Seed, buf, e); \
939+
XOR_deser(Size, buf, e); \
940+
r = binary_ ## fuse ## _allocate(Size, filter); \
941+
if (! r) \
942+
return (r); \
943+
filter->Seed = Seed; \
944+
const uint8_t *bitf = buf; \
945+
buf += XOR_bitf_sz(filter->ArrayLength); \
946+
for (size_t i = 0; i < filter->ArrayLength; i++) { \
947+
if ((bitf[XOR_bitf_word(i)] & XOR_bitf_bit(i)) == 0) \
948+
continue; \
949+
XOR_deser(filter->Fingerprints[i], buf, e); \
950+
} \
951+
return (true); \
952+
}
953+
954+
#define XOR_packers(fuse) \
955+
XOR_bytesf(fuse) \
956+
XOR_packf(fuse) \
957+
XOR_unpackf(fuse) \
958+
959+
XOR_packers(fuse8)
960+
XOR_packers(fuse16)
961+
962+
#undef XOR_packers
963+
#undef XOR_bytesf
964+
#undef XOR_packf
965+
#undef XOR_unpackf
966+
967+
#undef XOR_bitf_w
968+
#undef XOR_bitf_sz
969+
#undef XOR_bitf_word
970+
#undef XOR_bitf_bit
971+
#undef XOR_ser
972+
#undef XOR_deser
973+
861974
#endif

0 commit comments

Comments
 (0)