perf: #259 delta_compute hash index over signature blocks
This commit is contained in:
+140
-8
@@ -216,6 +216,119 @@ static void free_instructions(DeltaInstruction* instrs, uint32_t count) {
|
||||
free(instrs);
|
||||
}
|
||||
|
||||
/* Sentinel meaning "no signature block" in the lookup index chains. Block
|
||||
* counts are bounded well below UINT32_MAX, so it doubles as a null link. */
|
||||
#define DELTA_NO_BLOCK UINT32_MAX
|
||||
|
||||
/* Avalanche mix for the rolling checksum so blocks do not cluster in the
|
||||
* bucket table when the weak checksum has little entropy (e.g. all-zero or
|
||||
* patterned files). */
|
||||
static uint32_t delta_adler_mix(uint32_t h) {
|
||||
h ^= h >> 16;
|
||||
h *= 0x7feb352dU;
|
||||
h ^= h >> 15;
|
||||
h *= 0x846ca68bU;
|
||||
h ^= h >> 16;
|
||||
return h;
|
||||
}
|
||||
|
||||
/* Smallest power of two >= v. v must be non-zero. */
|
||||
static uint32_t delta_next_pow2(uint32_t v) {
|
||||
v--;
|
||||
v |= v >> 1;
|
||||
v |= v >> 2;
|
||||
v |= v >> 4;
|
||||
v |= v >> 8;
|
||||
v |= v >> 16;
|
||||
return v + 1;
|
||||
}
|
||||
|
||||
/* Build a hash index over sig->blocks keyed by the (mixed) rolling checksum.
|
||||
* All blocks sharing an Adler-32 value land in the same bucket; collisions
|
||||
* are chained through a single contiguous allocation:
|
||||
*
|
||||
* [0, bucket_count) heads (first block per bucket)
|
||||
* [bucket_count, 2*bucket_count) tails (last block per bucket)
|
||||
* [2*bucket_count, ...) per-block chain links
|
||||
*
|
||||
* Blocks are inserted in ascending index order so every bucket chain is
|
||||
* ordered exactly like the historical linear scan. Returns the base pointer
|
||||
* (also the heads array) or NULL when no index could be allocated; callers
|
||||
* then fall back to the linear scan. */
|
||||
static uint32_t* delta_build_index(const DeltaSignature* sig, uint32_t bucket_count) {
|
||||
if (sig->block_count == 0 || bucket_count == 0)
|
||||
return NULL;
|
||||
|
||||
size_t entries = (size_t)2 * bucket_count + sig->block_count;
|
||||
if (entries > SIZE_MAX / sizeof(uint32_t))
|
||||
return NULL;
|
||||
|
||||
uint32_t* index = protocol_alloc(entries * sizeof(uint32_t));
|
||||
if (!index)
|
||||
return NULL;
|
||||
|
||||
uint32_t* heads = index;
|
||||
uint32_t* tails = index + bucket_count;
|
||||
uint32_t* next = index + 2 * bucket_count;
|
||||
uint32_t mask = bucket_count - 1;
|
||||
|
||||
memset(heads, 0xFF, (size_t)bucket_count * sizeof(uint32_t));
|
||||
memset(tails, 0xFF, (size_t)bucket_count * sizeof(uint32_t));
|
||||
|
||||
for (uint32_t j = 0; j < sig->block_count; j++) {
|
||||
uint32_t b = delta_adler_mix(sig->blocks[j].adler32) & mask;
|
||||
if (heads[b] == DELTA_NO_BLOCK)
|
||||
heads[b] = j;
|
||||
else
|
||||
next[tails[b]] = j;
|
||||
tails[b] = j;
|
||||
next[j] = DELTA_NO_BLOCK;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/* Locate the signature block matching the byte window at new_data[i].
|
||||
*
|
||||
* Mirrors the original per-window behaviour exactly: only a full block_size
|
||||
* window can match, candidates are accepted only when the weak (Adler-32) and
|
||||
* strong (xxHash32) checksums both agree, and the lowest block index wins so
|
||||
* the emitted op stream is byte-identical to the linear scan. When heads is
|
||||
* non-NULL the candidate set is reached through the bucket index (expected
|
||||
* O(1) per window); otherwise an exact linear scan is used. */
|
||||
static uint32_t delta_find_match(const uint8_t* window, uint32_t window_len, uint32_t adler,
|
||||
bool full_window, const DeltaSignature* sig, const uint32_t* heads,
|
||||
const uint32_t* next, uint32_t mask) {
|
||||
if (!full_window || sig->block_count == 0)
|
||||
return DELTA_NO_BLOCK;
|
||||
|
||||
if (heads) {
|
||||
uint32_t b = delta_adler_mix(adler) & mask;
|
||||
uint32_t window_xxh = 0;
|
||||
bool have_xxh = false;
|
||||
for (uint32_t j = heads[b]; j != DELTA_NO_BLOCK; j = next[j]) {
|
||||
if (sig->blocks[j].adler32 != adler)
|
||||
continue;
|
||||
if (!have_xxh) {
|
||||
window_xxh = delta_xxhash32(window, window_len);
|
||||
have_xxh = true;
|
||||
}
|
||||
if (window_xxh == sig->blocks[j].xxhash)
|
||||
return j;
|
||||
}
|
||||
return DELTA_NO_BLOCK;
|
||||
}
|
||||
|
||||
/* Fallback used when the index could not be allocated. */
|
||||
for (uint32_t j = 0; j < sig->block_count; j++) {
|
||||
if (sig->blocks[j].adler32 == adler) {
|
||||
uint32_t window_xxh = delta_xxhash32(window, window_len);
|
||||
if (window_xxh == sig->blocks[j].xxhash)
|
||||
return j;
|
||||
}
|
||||
}
|
||||
return DELTA_NO_BLOCK;
|
||||
}
|
||||
|
||||
Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const DeltaSignature* sig,
|
||||
uint32_t block_size) {
|
||||
if (!new_file_data || !sig || !sig->blocks || new_file_size == 0 || block_size == 0 ||
|
||||
@@ -230,6 +343,25 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
if (!instrs)
|
||||
return NULL;
|
||||
|
||||
/* Build a one-time bucket index over the signature blocks keyed by the weak
|
||||
* checksum. This turns the per-byte-window candidate lookup from an
|
||||
* O(block_count) linear scan into an expected O(1) probe, which dominates
|
||||
* the cost for large mostly-matching files (the diff steps one byte at a
|
||||
* time through changed regions). On allocation failure the probe falls back
|
||||
* to the original linear scan, so behaviour is unchanged under memory
|
||||
* pressure. */
|
||||
uint32_t* index = NULL;
|
||||
const uint32_t* chain_next = NULL;
|
||||
uint32_t mask = 0;
|
||||
if (sig->block_count > 0) {
|
||||
uint32_t bucket_count = delta_next_pow2(sig->block_count);
|
||||
index = delta_build_index(sig, bucket_count);
|
||||
if (index) {
|
||||
chain_next = index + 2 * bucket_count;
|
||||
mask = bucket_count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t literal_start = 0;
|
||||
bool has_literal = false;
|
||||
|
||||
@@ -264,13 +396,13 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
}
|
||||
|
||||
bool matched = false;
|
||||
for (uint32_t j = 0; j < sig->block_count; j++) {
|
||||
if (adler == sig->blocks[j].adler32 && full_window) {
|
||||
uint32_t xxh = delta_xxhash32(new_data + i, window_len);
|
||||
if (xxh == sig->blocks[j].xxhash) {
|
||||
uint32_t match_block = delta_find_match(new_data + i, window_len, adler, full_window, sig,
|
||||
index, chain_next, mask);
|
||||
if (match_block != DELTA_NO_BLOCK) {
|
||||
if (has_literal) {
|
||||
if (!flush_literal(&instrs, &capacity, &count, new_data, literal_start, i)) {
|
||||
free_instructions(instrs, count);
|
||||
free(index);
|
||||
return NULL;
|
||||
}
|
||||
has_literal = false;
|
||||
@@ -278,10 +410,11 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
|
||||
if (!ensure_capacity(&instrs, &capacity, count)) {
|
||||
free_instructions(instrs, count);
|
||||
free(index);
|
||||
return NULL;
|
||||
}
|
||||
instrs[count].type = DELTA_INSTR_BLOCK_MATCH;
|
||||
instrs[count].match.block_index = j;
|
||||
instrs[count].match.block_index = match_block;
|
||||
instrs[count].match.block_offset = 0;
|
||||
instrs[count].match.length = window_len;
|
||||
count++;
|
||||
@@ -289,9 +422,6 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
i += window_len;
|
||||
rolling_valid = false;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
@@ -303,6 +433,8 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
}
|
||||
}
|
||||
|
||||
free(index);
|
||||
|
||||
if (has_literal) {
|
||||
if (!flush_literal(&instrs, &capacity, &count, new_data, literal_start, new_file_size)) {
|
||||
free_instructions(instrs, count);
|
||||
|
||||
@@ -343,6 +343,297 @@ static void test_delta_apply_rejects_output_overflow() {
|
||||
EXPECT_TRUE(delta_apply(old_data, sizeof(old_data), &delta, 1) == NULL);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Hash-index lookup differential tests.
|
||||
*
|
||||
* delta_compute buckets signature blocks by their weak checksum. These tests
|
||||
* prove the bucket-indexed candidate lookup is behaviour-identical to the
|
||||
* original per-window linear scan: the emitted instruction stream (types,
|
||||
* lengths, literal bytes and chosen block indices) must match a naive linear
|
||||
* reference exactly, and the delta must reconstruct the new buffer.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
#define REF_NO_MATCH UINT32_MAX
|
||||
|
||||
typedef struct {
|
||||
DeltaInstruction* items;
|
||||
uint32_t count;
|
||||
uint32_t cap;
|
||||
} RefDelta;
|
||||
|
||||
static void ref_delta_free(RefDelta* ref) {
|
||||
if (!ref->items)
|
||||
return;
|
||||
for (uint32_t i = 0; i < ref->count; i++)
|
||||
if (ref->items[i].type == DELTA_INSTR_LITERAL)
|
||||
free(ref->items[i].literal.data);
|
||||
free(ref->items);
|
||||
ref->items = NULL;
|
||||
ref->count = 0;
|
||||
ref->cap = 0;
|
||||
}
|
||||
|
||||
static bool ref_delta_push(RefDelta* ref, DeltaInstruction instr) {
|
||||
if (ref->count == ref->cap) {
|
||||
uint32_t new_cap = ref->cap ? ref->cap * 2 : 16;
|
||||
DeltaInstruction* tmp = realloc(ref->items, (size_t)new_cap * sizeof(DeltaInstruction));
|
||||
if (!tmp)
|
||||
return false;
|
||||
ref->items = tmp;
|
||||
ref->cap = new_cap;
|
||||
}
|
||||
ref->items[ref->count++] = instr;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ref_delta_flush_literal(RefDelta* ref, const uint8_t* data, uint64_t start,
|
||||
uint64_t end) {
|
||||
if (start >= end)
|
||||
return true;
|
||||
uint8_t* lit = malloc((size_t)(end - start));
|
||||
if (!lit)
|
||||
return false;
|
||||
memcpy(lit, data + start, (size_t)(end - start));
|
||||
DeltaInstruction instr = {
|
||||
.type = DELTA_INSTR_LITERAL,
|
||||
.literal = {.data = lit, .length = (uint32_t)(end - start)},
|
||||
};
|
||||
return ref_delta_push(ref, instr);
|
||||
}
|
||||
|
||||
/* Naive O(windows x blocks) re-implementation of the historical delta_compute
|
||||
* candidate scan: only full windows may match, a candidate needs both the weak
|
||||
* (Adler-32) and strong (xxHash32) checksums to agree, and the lowest matching
|
||||
* block index is selected. */
|
||||
static bool ref_delta_build(RefDelta* ref, const uint8_t* new_data, uint64_t new_size,
|
||||
const DeltaSignature* sig) {
|
||||
uint32_t block_size = sig->block_size;
|
||||
uint64_t i = 0;
|
||||
uint64_t literal_start = 0;
|
||||
bool has_literal = false;
|
||||
|
||||
while (i < new_size) {
|
||||
uint32_t window_len = (uint32_t)((new_size - i < block_size) ? (new_size - i) : block_size);
|
||||
bool full_window = (window_len == block_size);
|
||||
|
||||
uint32_t matched = REF_NO_MATCH;
|
||||
if (full_window) {
|
||||
uint32_t adler = delta_adler32(new_data + i, window_len);
|
||||
for (uint32_t j = 0; j < sig->block_count; j++) {
|
||||
if (sig->blocks[j].adler32 == adler &&
|
||||
delta_xxhash32(new_data + i, window_len) == sig->blocks[j].xxhash) {
|
||||
matched = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matched != REF_NO_MATCH) {
|
||||
if (has_literal) {
|
||||
if (!ref_delta_flush_literal(ref, new_data, literal_start, i))
|
||||
return false;
|
||||
has_literal = false;
|
||||
}
|
||||
DeltaInstruction instr = {
|
||||
.type = DELTA_INSTR_BLOCK_MATCH,
|
||||
.match = {.block_index = matched, .block_offset = 0, .length = window_len},
|
||||
};
|
||||
if (!ref_delta_push(ref, instr))
|
||||
return false;
|
||||
i += window_len;
|
||||
} else {
|
||||
if (!has_literal) {
|
||||
literal_start = i;
|
||||
has_literal = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_literal && !ref_delta_flush_literal(ref, new_data, literal_start, new_size))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ref_delta_matches(const RefDelta* ref, const Delta* delta) {
|
||||
if (ref->count != delta->instruction_count)
|
||||
return false;
|
||||
for (uint32_t i = 0; i < ref->count; i++) {
|
||||
const DeltaInstruction* a = &ref->items[i];
|
||||
const DeltaInstruction* b = &delta->instructions[i];
|
||||
if (a->type != b->type)
|
||||
return false;
|
||||
if (a->type == DELTA_INSTR_BLOCK_MATCH) {
|
||||
if (a->match.block_index != b->match.block_index ||
|
||||
a->match.block_offset != b->match.block_offset || a->match.length != b->match.length)
|
||||
return false;
|
||||
} else {
|
||||
if (a->literal.length != b->literal.length ||
|
||||
memcmp(a->literal.data, b->literal.data, a->literal.length) != 0)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void expect_linear_reference_match(const uint8_t* old_data, uint64_t old_size,
|
||||
const uint8_t* new_data, uint64_t new_size,
|
||||
uint32_t block_size, const char* label) {
|
||||
DeltaSignature* sig = delta_signature_create(old_data, old_size, block_size);
|
||||
if (!sig) {
|
||||
printf(" [FAIL] %s: signature creation failed\n", label);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
return;
|
||||
}
|
||||
Delta* delta = delta_compute(new_data, new_size, sig, block_size);
|
||||
if (!delta) {
|
||||
printf(" [FAIL] %s: delta_compute returned NULL\n", label);
|
||||
delta_signature_destroy(sig);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
return;
|
||||
}
|
||||
RefDelta ref = {0};
|
||||
bool ok = ref_delta_build(&ref, new_data, new_size, sig);
|
||||
if (ok)
|
||||
ok = ref_delta_matches(&ref, delta);
|
||||
if (!ok) {
|
||||
printf(" [FAIL] %s: instruction stream differs from linear reference "
|
||||
"(linear=%u indexed=%u)\n",
|
||||
label, ref.count, delta->instruction_count);
|
||||
}
|
||||
ref_delta_free(&ref);
|
||||
delta_destroy(delta);
|
||||
delta_signature_destroy(sig);
|
||||
EXPECT_TRUE(ok);
|
||||
}
|
||||
|
||||
static void fill_delta_pattern(uint8_t* buf, uint64_t size, uint32_t seed) {
|
||||
uint32_t x = seed ? seed : 1;
|
||||
for (uint64_t i = 0; i < size; i++) {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
buf[i] = (uint8_t)(x >> 24);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_delta_hash_index_matches_linear_reference() {
|
||||
/* Identical file (full block alignment). */
|
||||
uint8_t old_a[32768];
|
||||
uint8_t new_a[32768];
|
||||
fill_delta_pattern(old_a, sizeof(old_a), 42);
|
||||
memcpy(new_a, old_a, sizeof(old_a));
|
||||
expect_linear_reference_match(old_a, sizeof(old_a), new_a, sizeof(new_a), 2048,
|
||||
"identical 32KiB @ 2KiB");
|
||||
|
||||
/* Scattered single-byte edits in the middle of each block. */
|
||||
uint8_t new_b[32768];
|
||||
memcpy(new_b, old_a, sizeof(old_a));
|
||||
for (size_t p = 100; p < sizeof(new_b); p += 4096)
|
||||
new_b[p] ^= 0x5A;
|
||||
expect_linear_reference_match(old_a, sizeof(old_a), new_b, sizeof(new_b), 2048,
|
||||
"32KiB scattered single-byte edits @ 2KiB");
|
||||
|
||||
/* Non-aligned old file (partial final block) with a single edit. */
|
||||
uint8_t old_c[30000];
|
||||
uint8_t new_c[30000];
|
||||
fill_delta_pattern(old_c, sizeof(old_c), 7);
|
||||
memcpy(new_c, old_c, sizeof(old_c));
|
||||
new_c[15000] ^= 0x3C;
|
||||
expect_linear_reference_match(old_c, sizeof(old_c), new_c, sizeof(new_c), 2048,
|
||||
"30KiB partial-tail single edit @ 2KiB");
|
||||
|
||||
/* Growth: appended data after an identical prefix. */
|
||||
uint8_t old_d[24576];
|
||||
uint8_t new_d[34576];
|
||||
fill_delta_pattern(old_d, sizeof(old_d), 11);
|
||||
memcpy(new_d, old_d, sizeof(old_d));
|
||||
fill_delta_pattern(new_d + sizeof(old_d), sizeof(new_d) - sizeof(old_d), 23);
|
||||
expect_linear_reference_match(old_d, sizeof(old_d), new_d, sizeof(new_d), 2048,
|
||||
"24KiB -> 34KiB appended @ 2KiB");
|
||||
|
||||
/* Insertion shifting everything after the edit point (rsync re-sync). */
|
||||
uint8_t old_e[65536];
|
||||
uint8_t new_e[65536 + 3000];
|
||||
fill_delta_pattern(old_e, sizeof(old_e), 99);
|
||||
memcpy(new_e, old_e, 20000);
|
||||
fill_delta_pattern(new_e + 20000, 3000, 101);
|
||||
memcpy(new_e + 23000, old_e + 20000, sizeof(old_e) - 20000);
|
||||
expect_linear_reference_match(old_e, sizeof(old_e), new_e, sizeof(new_e), 2048,
|
||||
"64KiB + 3KiB insertion @ 2KiB");
|
||||
|
||||
/* Deletion shrinking the file. */
|
||||
uint8_t new_f[sizeof(old_e) - 5000];
|
||||
memcpy(new_f, old_e, 30000);
|
||||
memcpy(new_f + 30000, old_e + 35000, sizeof(old_e) - 35000);
|
||||
expect_linear_reference_match(old_e, sizeof(old_e), new_f, sizeof(new_f), 2048,
|
||||
"64KiB - 5KiB deletion @ 2KiB");
|
||||
|
||||
/* Repeated identical blocks must resolve to the lowest block index. */
|
||||
uint8_t old_g[4 * 4096];
|
||||
uint8_t new_g[4 * 4096];
|
||||
for (uint32_t b = 0; b < 4; b++)
|
||||
fill_delta_pattern(old_g + b * 4096, 4096, b % 2 == 0 ? 500 : 501); /* block0==block2 */
|
||||
memcpy(new_g, old_g, sizeof(old_g));
|
||||
new_g[4096 + 5] ^= 0x11; /* edit inside the second (duplicated) chunk */
|
||||
expect_linear_reference_match(old_g, sizeof(old_g), new_g, sizeof(new_g), 4096,
|
||||
"duplicated chunks @ 4KiB");
|
||||
|
||||
/* Block larger than the file: nothing can match, all literal. */
|
||||
uint8_t old_h[1000];
|
||||
uint8_t new_h[1000];
|
||||
fill_delta_pattern(old_h, sizeof(old_h), 3);
|
||||
memcpy(new_h, old_h, sizeof(old_h));
|
||||
expect_linear_reference_match(old_h, sizeof(old_h), new_h, sizeof(new_h), 4096,
|
||||
"1KiB file @ 4KiB block");
|
||||
}
|
||||
|
||||
static void test_delta_hash_index_large_mostly_matching() {
|
||||
const uint64_t size = 4ULL * 1024 * 1024;
|
||||
const uint32_t block_size = 8192;
|
||||
|
||||
uint8_t* old_data = malloc((size_t)size);
|
||||
uint8_t* new_data = malloc((size_t)size);
|
||||
EXPECT_TRUE(old_data != NULL && new_data != NULL);
|
||||
|
||||
fill_delta_pattern(old_data, size, 1234);
|
||||
memcpy(new_data, old_data, (size_t)size);
|
||||
|
||||
/* Scattered single-byte changes across the whole buffer. Each change forces
|
||||
* the diff to re-synchronise by walking one byte at a time through the
|
||||
* affected block, which is exactly the case that used to cost O(bytes x
|
||||
* blocks) with the linear scan. */
|
||||
const uint64_t nchanges = 64;
|
||||
for (uint64_t c = 0; c < nchanges; c++) {
|
||||
uint64_t pos = (c * (size / nchanges)) + (c % 17);
|
||||
new_data[pos] ^= (uint8_t)(0xA0 + (c % 16));
|
||||
}
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, size, block_size);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
EXPECT_EQ_INT((int)sig->block_count, (int)(size / block_size));
|
||||
|
||||
Delta* delta = delta_compute(new_data, size, sig, block_size);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
EXPECT_EQ_INT((int)delta->new_file_size, (int)size);
|
||||
|
||||
uint32_t match_count = 0;
|
||||
for (uint32_t i = 0; i < delta->instruction_count; i++)
|
||||
if (delta->instructions[i].type == DELTA_INSTR_BLOCK_MATCH)
|
||||
match_count++;
|
||||
EXPECT_TRUE(match_count > 0);
|
||||
|
||||
void* result = delta_apply(old_data, size, delta, block_size);
|
||||
EXPECT_NOT_NULL(result);
|
||||
EXPECT_EQ_INT(memcmp(result, new_data, (size_t)size), 0);
|
||||
|
||||
free(result);
|
||||
delta_destroy(delta);
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
free(new_data);
|
||||
}
|
||||
|
||||
void test_delta() {
|
||||
test_adler32_basic();
|
||||
test_adler32_different_data();
|
||||
@@ -360,4 +651,6 @@ void test_delta() {
|
||||
test_is_worthwhile();
|
||||
test_large_file_delta();
|
||||
test_delta_apply_rejects_output_overflow();
|
||||
test_delta_hash_index_matches_linear_reference();
|
||||
test_delta_hash_index_large_mostly_matching();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user