Add XXH3-64 + tests.

This commit is contained in:
Jeroen van Rijn
2021-09-13 20:58:26 +02:00
parent e9b9d15de7
commit a641ef95c0
2 changed files with 170 additions and 126 deletions
+142 -126
View File
@@ -11,22 +11,24 @@ package xxhash
import "core:intrinsics" import "core:intrinsics"
/* ********************************************************************* /*
* XXH3 *************************************************************************
* New generation hash designed for speed on small keys and vectorization * XXH3
************************************************************************ * New generation hash designed for speed on small keys and vectorization
*************************************************************************
* One goal of XXH3 is to make it fast on both 32-bit and 64-bit, while * One goal of XXH3 is to make it fast on both 32-bit and 64-bit, while
* remaining a true 64-bit/128-bit hash function. * remaining a true 64-bit/128-bit hash function.
* ==========================================
* XXH3 default settings
* ==========================================
*/ */
/* ========================================== /*
* XXH3 default settings Custom secrets have a default length of 192, but can be set to a different size.
* ========================================== */ The minimum secret size is 136 bytes. It must also be a multiple of 64.
*/
XXH_ACC_ALIGN :: 8 /* scalar */
XXH3_SECRET_SIZE_MIN :: 136
XXH_SECRET_DEFAULT_SIZE :: max(XXH3_SECRET_SIZE_MIN, #config(XXH_SECRET_DEFAULT_SIZE, 192)) XXH_SECRET_DEFAULT_SIZE :: max(XXH3_SECRET_SIZE_MIN, #config(XXH_SECRET_DEFAULT_SIZE, 192))
#assert(XXH_SECRET_DEFAULT_SIZE % 64 == 0)
XXH3_kSecret :: [?]u8{ XXH3_kSecret :: [?]u8{
0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c, 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c,
@@ -42,8 +44,42 @@ XXH3_kSecret :: [?]u8{
0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce, 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce,
0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e, 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e,
} }
#assert(size_of(XXH3_kSecret) == 192) /*
Do not change this constant.
*/
XXH3_SECRET_SIZE_MIN :: 136
#assert(size_of(XXH3_kSecret) == 192 && size_of(XXH3_kSecret) > XXH3_SECRET_SIZE_MIN)
XXH_ACC_ALIGN :: 8 /* scalar */
/*
This is the optimal update size for incremental hashing.
*/
XXH3_INTERNAL_BUFFER_SIZE :: 256
/*
Streaming state.
IMPORTANT: This structure has a strict alignment requirement of 64 bytes!! **
Do not allocate this with `make()` or `new`, it will not be sufficiently aligned.
Use`XXH3_create_state` and `XXH3_destroy_state, or stack allocation.
*/
XXH3_state :: struct {
acc: [8]u64,
custom_secret: [XXH_SECRET_DEFAULT_SIZE]u8,
buffer: [XXH3_INTERNAL_BUFFER_SIZE]u8,
buffered_size: u32,
reserved32: u32,
stripes_so_far: uint,
total_len: u64,
stripes_per_block: uint,
secret_limit: uint,
seed: u64,
reserved64: u64,
external_secret: ^[]u8,
}
#assert(offset_of(XXH3_state, acc) % 64 == 0 && offset_of(XXH3_state, custom_secret) % 64 == 0 &&
offset_of(XXH3_state, buffer) % 64 == 0)
/************************************************************************ /************************************************************************
* XXH3 128-bit variant * XXH3 128-bit variant
@@ -54,7 +90,6 @@ XXH3_kSecret :: [?]u8{
*/ */
xxh_u128 :: u128 xxh_u128 :: u128
XXH3_128_hash :: u128 XXH3_128_hash :: u128
XXH3_128_DEFAULT_SEED :: xxh_u64(0)
XXH128_hash_t :: struct #raw_union { XXH128_hash_t :: struct #raw_union {
using raw: struct { using raw: struct {
@@ -436,7 +471,12 @@ XXH3_128bits_internal :: #force_inline proc(
/* === Public XXH128 API === */ /* === Public XXH128 API === */
XXH3_128_with_seed :: proc(input: []u8, seed := XXH3_128_DEFAULT_SEED) -> (hash: XXH3_128_hash) { XXH3_128_default :: proc(input: []u8) -> (hash: XXH3_128_hash) {
k := XXH3_kSecret
return XXH3_128bits_internal(input, 0, k[:], XXH3_hashLong_128b_withSeed)
}
XXH3_128_with_seed :: proc(input: []u8, seed: xxh_u64) -> (hash: XXH3_128_hash) {
k := XXH3_kSecret k := XXH3_kSecret
return XXH3_128bits_internal(input, seed, k[:], XXH3_hashLong_128b_withSeed) return XXH3_128bits_internal(input, seed, k[:], XXH3_hashLong_128b_withSeed)
} }
@@ -444,18 +484,17 @@ XXH3_128_with_seed :: proc(input: []u8, seed := XXH3_128_DEFAULT_SEED) -> (hash:
XXH3_128_with_secret :: proc(input: []u8, secret: []u8) -> (hash: XXH3_128_hash) { XXH3_128_with_secret :: proc(input: []u8, secret: []u8) -> (hash: XXH3_128_hash) {
return XXH3_128bits_internal(input, 0, secret, XXH3_hashLong_128b_withSecret) return XXH3_128bits_internal(input, 0, secret, XXH3_hashLong_128b_withSecret)
} }
XXH3_128 :: proc { XXH3_128_with_seed, XXH3_128_with_secret } XXH3_128 :: proc { XXH3_128_default, XXH3_128_with_seed, XXH3_128_with_secret }
/*
/* === XXH3 128-bit streaming === */ /* === XXH3 128-bit streaming === */
/* /*
* All the functions are actually the same as for 64-bit streaming variant. All the functions are actually the same as for 64-bit streaming variant.
* The only difference is the finalization routine. The only difference is the finalization routine.
*/ */
/*
/*! @ingroup xxh3_family */
XXH_PUBLIC_API XXH_errorcode XXH_PUBLIC_API XXH_errorcode
XXH3_128bits_reset(XXH3_state_t* statePtr) XXH3_128bits_reset(XXH3_state_t* statePtr)
{ {
@@ -635,7 +674,8 @@ XXH3_len_4to8_64b :: #force_inline proc(input: []u8, secret: []u8, seed: xxh_u64
assert(secret != nil) assert(secret != nil)
seed := seed seed := seed
seed ~= u64(byte_swap(u32(seed) << 32)) seed ~= (u64(byte_swap(u32(seed))) << 32)
#no_bounds_check { #no_bounds_check {
input1 := XXH32_read32(input) input1 := XXH32_read32(input)
input2 := XXH32_read32(input[length - 4:]) input2 := XXH32_read32(input[length - 4:])
@@ -907,7 +947,6 @@ XXH3_hashLong_internal_loop :: #force_inline proc(acc: []xxh_u64, input: []u8, s
/* last partial block */ /* last partial block */
#no_bounds_check { #no_bounds_check {
stripes := ((length - 1) - (block_len * blocks)) / XXH_STRIPE_LEN stripes := ((length - 1) - (block_len * blocks)) / XXH_STRIPE_LEN
XXH3_accumulate(acc, input[blocks * block_len:], secret, stripes, f_acc512) XXH3_accumulate(acc, input[blocks * block_len:], secret, stripes, f_acc512)
/* last stripe */ /* last stripe */
@@ -935,142 +974,119 @@ XXH3_mergeAccs :: #force_inline proc(acc: []xxh_u64, secret: []u8, start: xxh_u6
return XXH3_avalanche(result64) return XXH3_avalanche(result64)
} }
@(optimization_mode="speed")
XXH3_hashLong_64b_internal :: #force_inline proc(input: []u8, secret: []u8,
f_acc512: XXH3_accumulate_512_f, f_scramble: XXH3_scramble_accumulator_f) -> (hash: xxh_u64) {
/* acc: [XXH_ACC_NB]xxh_u64 = XXH3_INIT_ACC
XXH_FORCE_INLINE XXH64_hash_t
XXH3_hashLong_64b_internal(const void* XXH_RESTRICT input, size_t len,
const void* XXH_RESTRICT secret, size_t secretSize,
XXH3_f_accumulate_512 f_acc512,
XXH3_f_scrambleAcc f_scramble)
{
XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[XXH_ACC_NB] = XXH3_INIT_ACC;
XXH3_hashLong_internal_loop(acc, (const xxh_u8*)input, len, (const xxh_u8*)secret, secretSize, f_acc512, f_scramble); XXH3_hashLong_internal_loop(acc[:], input, secret, f_acc512, f_scramble)
/* converge into final hash */ /* converge into final hash */
XXH_STATIC_ASSERT(sizeof(acc) == 64); #assert(size_of(acc) == 64)
/* do not align on 8, so that the secret is different from the accumulator */ /* do not align on 8, so that the secret is different from the accumulator */
#define XXH_SECRET_MERGEACCS_START 11 XXH_SECRET_MERGEACCS_START :: 11
XXH_ASSERT(secretSize >= sizeof(acc) + XXH_SECRET_MERGEACCS_START); assert(len(secret) >= size_of(acc) + XXH_SECRET_MERGEACCS_START)
return XXH3_mergeAccs(acc, (const xxh_u8*)secret + XXH_SECRET_MERGEACCS_START, (xxh_u64)len * XXH_PRIME64_1); return XXH3_mergeAccs(acc[:], secret[XXH_SECRET_MERGEACCS_START:], xxh_u64(len(input)) * XXH_PRIME64_1)
} }
/* /*
* It's important for performance that XXH3_hashLong is not inlined. It's important for performance that XXH3_hashLong is not inlined.
*/ */
XXH_NO_INLINE XXH64_hash_t XXH3_hashLong_64b_withSecret :: #force_no_inline proc(input: []u8, seed64: xxh_u64, secret: []u8) -> (hash: xxh_u64) {
XXH3_hashLong_64b_withSecret(const void* XXH_RESTRICT input, size_t len, return XXH3_hashLong_64b_internal(input, secret, XXH3_accumulate_512, XXH3_scramble_accumulator)
XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen)
{
(void)seed64;
return XXH3_hashLong_64b_internal(input, len, secret, secretLen, XXH3_accumulate_512, XXH3_scrambleAcc);
} }
/* /*
* It's important for performance that XXH3_hashLong is not inlined. It's important for performance that XXH3_hashLong is not inlined.
* Since the function is not inlined, the compiler may not be able to understand that, Since the function is not inlined, the compiler may not be able to understand that,
* in some scenarios, its `secret` argument is actually a compile time constant. in some scenarios, its `secret` argument is actually a compile time constant.
* This variant enforces that the compiler can detect that, This variant enforces that the compiler can detect that,
* and uses this opportunity to streamline the generated code for better performance. and uses this opportunity to streamline the generated code for better performance.
*/ */
XXH_NO_INLINE XXH64_hash_t XXH3_hashLong_64b_default :: #force_no_inline proc(input: []u8, seed64: xxh_u64, secret: []u8) -> (hash: xxh_u64) {
XXH3_hashLong_64b_default(const void* XXH_RESTRICT input, size_t len, k := XXH3_kSecret
XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen) return XXH3_hashLong_64b_internal(input, k[:], XXH3_accumulate_512, XXH3_scramble_accumulator)
{
(void)seed64; (void)secret; (void)secretLen;
return XXH3_hashLong_64b_internal(input, len, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_accumulate_512, XXH3_scrambleAcc);
} }
/* /*
* XXH3_hashLong_64b_withSeed(): XXH3_hashLong_64b_withSeed():
* Generate a custom key based on alteration of default XXH3_kSecret with the seed, Generate a custom key based on alteration of default XXH3_kSecret with the seed,
* and then use this key for long mode hashing. and then use this key for long mode hashing.
*
* This operation is decently fast but nonetheless costs a little bit of time. This operation is decently fast but nonetheless costs a little bit of time.
* Try to avoid it whenever possible (typically when seed==0). Try to avoid it whenever possible (typically when seed==0).
*
* It's important for performance that XXH3_hashLong is not inlined. Not sure It's important for performance that XXH3_hashLong is not inlined. Not sure
* why (uop cache maybe?), but the difference is large and easily measurable. why (uop cache maybe?), but the difference is large and easily measurable.
*/ */
XXH_FORCE_INLINE XXH64_hash_t XXH3_hashLong_64b_withSeed_internal :: #force_no_inline proc(input: []u8,
XXH3_hashLong_64b_withSeed_internal(const void* input, size_t len, seed: xxh_u64,
XXH64_hash_t seed, f_acc512: XXH3_accumulate_512_f,
XXH3_f_accumulate_512 f_acc512, f_scramble: XXH3_scramble_accumulator_f,
XXH3_f_scrambleAcc f_scramble, f_init_sec: XXH3_init_custom_secret_f) -> (hash: xxh_u64) {
XXH3_f_initCustomSecret f_initSec) if seed == 0 {
{ k := XXH3_kSecret
if (seed == 0) return XXH3_hashLong_64b_internal(input, k[:], f_acc512, f_scramble)
return XXH3_hashLong_64b_internal(input, len, }
XXH3_kSecret, sizeof(XXH3_kSecret), {
f_acc512, f_scramble); secret: [XXH_SECRET_DEFAULT_SIZE]u8
{ XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE]; f_init_sec(secret[:], seed)
f_initSec(secret, seed); return XXH3_hashLong_64b_internal(input, secret[:], f_acc512, f_scramble)
return XXH3_hashLong_64b_internal(input, len, secret, sizeof(secret),
f_acc512, f_scramble);
} }
} }
/* /*
* It's important for performance that XXH3_hashLong is not inlined. It's important for performance that XXH3_hashLong is not inlined.
*/ */
XXH_NO_INLINE XXH64_hash_t XXH3_hashLong_64b_withSeed :: #force_no_inline proc(input: []u8, seed: xxh_u64, secret: []u8) -> (hash: xxh_u64) {
XXH3_hashLong_64b_withSeed(const void* input, size_t len, return XXH3_hashLong_64b_withSeed_internal(input, seed, XXH3_accumulate_512, XXH3_scramble_accumulator, XXH3_init_custom_secret)
XXH64_hash_t seed, const xxh_u8* secret, size_t secretLen)
{
(void)secret; (void)secretLen;
return XXH3_hashLong_64b_withSeed_internal(input, len, seed,
XXH3_accumulate_512, XXH3_scrambleAcc, XXH3_initCustomSecret);
} }
typedef XXH64_hash_t (*XXH3_hashLong64_f)(const void* XXH_RESTRICT, size_t, XXH3_hashLong64_f :: #type proc(input: []u8, seed: xxh_u64, secret: []u8) -> (res: xxh_u64)
XXH64_hash_t, const xxh_u8* XXH_RESTRICT, size_t);
XXH_FORCE_INLINE XXH64_hash_t XXH3_64bits_internal :: proc(input: []u8, seed: xxh_u64, secret: []u8, f_hashLong: XXH3_hashLong64_f) -> (hash: xxh_u64) {
XXH3_64bits_internal(const void* XXH_RESTRICT input, size_t len,
XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen,
XXH3_hashLong64_f f_hashLong)
{
XXH_ASSERT(secretLen >= XXH3_SECRET_SIZE_MIN); assert(len(secret) >= XXH3_SECRET_SIZE_MIN)
/* /*
* If an action is to be taken if `secretLen` condition is not respected, If an action is to be taken if len(secret) condition is not respected, it should be done here.
* it should be done here. For now, it's a contract pre-condition.
* For now, it's a contract pre-condition. Adding a check and a branch here would cost performance at every hash.
* Adding a check and a branch here would cost performance at every hash. Also, note that function signature doesn't offer room to return an error.
* Also, note that function signature doesn't offer room to return an error. */
*/ length := len(input)
if (len <= 16) switch {
return XXH3_len_0to16_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, seed64); case length <= 16: return XXH3_len_0to16_64b(input, secret, seed)
if (len <= 128) case length <= 128: return XXH3_len_17to128_64b(input, secret, seed)
return XXH3_len_17to128_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); case length <= XXH3_MIDSIZE_MAX: return XXH3_len_129to240_64b(input, secret, seed)
if (len <= XXH3_MIDSIZE_MAX) case: return f_hashLong(input, seed, secret)
return XXH3_len_129to240_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); }
return f_hashLong(input, len, seed64, (const xxh_u8*)secret, secretLen); unreachable()
} }
/* === Public entry point === */ /* === Public entry point === */
/*! @ingroup xxh3_family */ XXH3_64_default :: proc(input: []u8) -> (hash: xxh_u64) {
XXH_PUBLIC_API XXH64_hash_t XXH3_64bits(const void* input, size_t len) k := XXH3_kSecret
{ return XXH3_64bits_internal(input, 0, k[:], XXH3_hashLong_64b_default)
return XXH3_64bits_internal(input, len, 0, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_default);
} }
/*! @ingroup xxh3_family */ XXH3_64_with_seed :: proc(input: []u8, seed: xxh_u64) -> (hash: xxh_u64) {
XXH_PUBLIC_API XXH64_hash_t k := XXH3_kSecret
XXH3_64bits_withSecret(const void* input, size_t len, const void* secret, size_t secretSize) return XXH3_64bits_internal(input, seed, k[:], XXH3_hashLong_64b_withSeed)
{
return XXH3_64bits_internal(input, len, 0, secret, secretSize, XXH3_hashLong_64b_withSecret);
} }
/*! @ingroup xxh3_family */ XXH3_64_with_secret :: proc(input, secret: []u8) -> (hash: xxh_u64) {
XXH_PUBLIC_API XXH64_hash_t return XXH3_64bits_internal(input, 0, secret, XXH3_hashLong_64b_withSecret)
XXH3_64bits_withSeed(const void* input, size_t len, XXH64_hash_t seed)
{
return XXH3_64bits_internal(input, len, seed, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_withSeed);
} }
XXH3_64 :: proc { XXH3_64_default, XXH3_64_with_seed, XXH3_64_with_secret }
/*
/* === XXH3 streaming === */ /* === XXH3 streaming === */
+28
View File
@@ -79,6 +79,19 @@ benchmark_xxh64 :: proc(options: ^time.Benchmark_Options, allocator := context.a
return nil return nil
} }
benchmark_xxh3_64 :: proc(options: ^time.Benchmark_Options, allocator := context.allocator) -> (err: time.Benchmark_Error) {
buf := options.input
h: u64
for _ in 0..=options.rounds {
h = xxhash.XXH3_64(buf)
}
options.count = options.rounds
options.processed = options.rounds * options.bytes
options.hash = u128(h)
return nil
}
benchmark_xxh3_128 :: proc(options: ^time.Benchmark_Options, allocator := context.allocator) -> (err: time.Benchmark_Error) { benchmark_xxh3_128 :: proc(options: ^time.Benchmark_Options, allocator := context.allocator) -> (err: time.Benchmark_Error) {
buf := options.input buf := options.input
@@ -143,6 +156,21 @@ test_benchmark_runner :: proc(t: ^testing.T) {
expect(t, options.hash == 0x87d2a1b6e1163ef1, name) expect(t, options.hash == 0x87d2a1b6e1163ef1, name)
benchmark_print(name, options) benchmark_print(name, options)
name = "XXH3_64 100 zero bytes"
options.bytes = 100
options.bench = benchmark_xxh3_64
err = time.benchmark(options, context.allocator)
expect(t, err == nil, name)
expect(t, options.hash == 0x801fedc74ccd608c, name)
benchmark_print(name, options)
name = "XXH3_64 1 MiB zero bytes"
options.bytes = 1_048_576
err = time.benchmark(options, context.allocator)
expect(t, err == nil, name)
expect(t, options.hash == 0x918780b90550bf34, name)
benchmark_print(name, options)
name = "XXH3_128 100 zero bytes" name = "XXH3_128 100 zero bytes"
options.bytes = 100 options.bytes = 100
options.bench = benchmark_xxh3_128 options.bench = benchmark_xxh3_128