Skip to content

Commit a23ff47

Browse files
committed
add safer, more versatile version of rapidhash for any array and for any iterable
Tested that it gives the same answer on random data, and that performance is equivalent (with LLVM 20 on AArch64). ``` julia> using BenchmarkTools, Random; for n = 0:67:1000 a = (i % UInt8 for i in 1:n) b = collect(a) ## Same results for [] and String # b = codeunits(randstring(n)) # a = Base.Generator(identity, b) @Btime Base.hash_bytes($a, Base.HASH_SEED, Base.HASH_SECRET) @Btime Base.hash_bytes($b, Base.HASH_SEED, Base.HASH_SECRET) @Btime Base.hash_bytes(pointer($b), length($b), Base.HASH_SEED, Base.HASH_SECRET) println() end 5.666 ns (0 allocations: 0 bytes) # iterator 3.625 ns (0 allocations: 0 bytes) # array 3.625 ns (0 allocations: 0 bytes) # pointer 20.269 ns (0 allocations: 0 bytes) 5.375 ns (0 allocations: 0 bytes) 5.083 ns (0 allocations: 0 bytes) 35.624 ns (0 allocations: 0 bytes) 6.250 ns (0 allocations: 0 bytes) 6.208 ns (0 allocations: 0 bytes) 50.954 ns (0 allocations: 0 bytes) 7.625 ns (0 allocations: 0 bytes) 7.334 ns (0 allocations: 0 bytes) 66.496 ns (0 allocations: 0 bytes) 9.083 ns (0 allocations: 0 bytes) 8.875 ns (0 allocations: 0 bytes) 82.299 ns (0 allocations: 0 bytes) 10.719 ns (0 allocations: 0 bytes) 10.511 ns (0 allocations: 0 bytes) 98.346 ns (0 allocations: 0 bytes) 12.888 ns (0 allocations: 0 bytes) 12.513 ns (0 allocations: 0 bytes) ```
1 parent edf661f commit a23ff47

File tree

2 files changed

+292
-2
lines changed

2 files changed

+292
-2
lines changed

base/hashing.jl

Lines changed: 273 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This file is a part of Julia. License is MIT: https://julialang.org/license
22

33
const HASH_SEED = UInt == UInt64 ? 0xbdd89aa982704029 : 0xeabe9406
4-
const HASH_SECRET = tuple(
4+
const HASH_SECRET = (
55
0x2d358dccaa6c78a5,
66
0x8bb84b93962eacc9,
77
0x4b33a62ed433d4a3,
@@ -73,7 +73,7 @@ hash(x::Union{Bool, Int8, UInt8, Int16, UInt16, Int32, UInt32}, h::UInt) = hash(
7373
hash_integer(x::Integer, h::UInt) = _hash_integer(x, UInt64(h)) % UInt
7474
function _hash_integer(
7575
x::Integer,
76-
seed::UInt64 = HASH_SEED,
76+
seed::UInt64,
7777
secret::NTuple{4, UInt64} = HASH_SECRET
7878
)
7979
seed ⊻= (x < 0)
@@ -344,6 +344,277 @@ load_le(::Type{T}, ptr::Ptr{UInt8}, i) where {T <: Union{UInt32, UInt64}} =
344344
return hash_mix(a secret[4], b secret[2] i)
345345
end
346346

347+
@inline function load_le_array(::Type{UInt64}, arr::AbstractArray{UInt8}, idx)
348+
# n.b. for whatever reason, writing this as a loop ensures LLVM
349+
# optimizations (particular SROA) don't make a disaster of this code
350+
# early on so it can actually emit the optimal result
351+
result = zero(UInt64)
352+
for i in 0:7
353+
byte = @inbounds arr[idx + i]
354+
result |= UInt64(byte) << (8 * i)
355+
end
356+
return result
357+
end
358+
359+
@inline function load_le_array(::Type{UInt32}, arr::AbstractArray{UInt8}, idx)
360+
result = zero(UInt32)
361+
for i in 0:3
362+
byte = @inbounds arr[idx + i]
363+
result |= UInt32(byte) << (8 * i)
364+
end
365+
return result
366+
end
367+
368+
@assume_effects :terminates_globally function hash_bytes(
369+
arr::AbstractArray{UInt8},
370+
seed::UInt64,
371+
secret::NTuple{4, UInt64}
372+
)
373+
# Adapted with gratitude from [rapidhash](https://github.com/Nicoshev/rapidhash)
374+
n = length(arr)
375+
buflen = UInt64(n)
376+
seed = seed hash_mix(seed secret[3], secret[2])
377+
firstidx = firstindex(arr)
378+
379+
a = zero(UInt64)
380+
b = zero(UInt64)
381+
i = buflen
382+
383+
if buflen 16
384+
if buflen 4
385+
seed ⊻= buflen
386+
if buflen 8
387+
a = load_le_array(UInt64, arr, firstidx)
388+
b = load_le_array(UInt64, arr, firstidx + n - 8)
389+
else
390+
a = UInt64(load_le_array(UInt32, arr, firstidx))
391+
b = UInt64(load_le_array(UInt32, arr, firstidx + n - 4))
392+
end
393+
elseif buflen > 0
394+
a = (UInt64(@inbounds arr[firstidx]) << 45) | UInt64(@inbounds arr[firstidx + n - 1])
395+
b = UInt64(@inbounds arr[firstidx + div(n, 2)])
396+
end
397+
else
398+
pos = 0
399+
if i > 48
400+
see1 = seed
401+
see2 = seed
402+
while i > 48
403+
seed = hash_mix(
404+
load_le_array(UInt64, arr, firstidx + pos) secret[1],
405+
load_le_array(UInt64, arr, firstidx + pos + 8) seed
406+
)
407+
see1 = hash_mix(
408+
load_le_array(UInt64, arr, firstidx + pos + 16) secret[2],
409+
load_le_array(UInt64, arr, firstidx + pos + 24) see1
410+
)
411+
see2 = hash_mix(
412+
load_le_array(UInt64, arr, firstidx + pos + 32) secret[3],
413+
load_le_array(UInt64, arr, firstidx + pos + 40) see2
414+
)
415+
pos += 48
416+
i -= 48
417+
end
418+
seed ⊻= see1
419+
seed ⊻= see2
420+
end
421+
if i > 16
422+
seed = hash_mix(
423+
load_le_array(UInt64, arr, firstidx + pos) secret[3],
424+
load_le_array(UInt64, arr, firstidx + pos + 8) seed
425+
)
426+
if i > 32
427+
seed = hash_mix(
428+
load_le_array(UInt64, arr, firstidx + pos + 16) secret[3],
429+
load_le_array(UInt64, arr, firstidx + pos + 24) seed
430+
)
431+
end
432+
end
433+
434+
a = load_le_array(UInt64, arr, firstidx + n - 16) i
435+
b = load_le_array(UInt64, arr, firstidx + n - 8)
436+
end
437+
438+
a = a secret[2]
439+
b = b seed
440+
b, a = mul_parts(a, b)
441+
return hash_mix(a secret[4], b secret[2] i)
442+
end
443+
444+
445+
# Helper function to concatenate two UInt64 values with a byte shift
446+
# Returns the result of shifting 'low' right by 'shift_bytes' bytes and
447+
# filling the high bits with the low bits of 'high'
448+
@inline function concat_shift(low::UInt64, high::UInt64, shift_bytes::UInt8)
449+
shift_bits = (shift_bytes * 0x8) & 0x3f
450+
return (low >> shift_bits) | (high << (0x40 - shift_bits))
451+
end
452+
453+
@inline function read_uint64_from_uint8_iter(iter, state)
454+
value = zero(UInt64)
455+
@nexprs 8 i -> begin
456+
next_result = iterate(iter, state)
457+
next_result === nothing && return value, state, UInt8(i - 1)
458+
byte, state = next_result
459+
value |= UInt64(byte) << ((i - 1) * 8)
460+
end
461+
return value, state, 0x8
462+
end
463+
464+
@inline function read_uint64_from_uint8_iter(iter)
465+
next_result = iterate(iter)
466+
next_result === nothing && return nothing
467+
byte, state = next_result
468+
value = UInt64(byte)
469+
@nexprs 7 i -> begin
470+
next_result = iterate(iter, state)
471+
next_result === nothing && return value, state, UInt8(i)
472+
byte, state = next_result
473+
value |= UInt64(byte::UInt8) << (i * 8)
474+
end
475+
return value, state, 0x8
476+
end
477+
478+
@assume_effects :terminates_globally function hash_bytes(
479+
iter,
480+
seed::UInt64,
481+
secret::NTuple{4, UInt64}
482+
)
483+
seed = seed hash_mix(seed secret[3], secret[2])
484+
485+
a = zero(UInt64)
486+
b = zero(UInt64)
487+
buflen = zero(UInt64)
488+
489+
see1 = seed
490+
see2 = seed
491+
l0 = zero(UInt64)
492+
l1 = zero(UInt64)
493+
l2 = zero(UInt64)
494+
l3 = zero(UInt64)
495+
l4 = zero(UInt64)
496+
l5 = zero(UInt64)
497+
b0 = 0x0
498+
b1 = 0x0
499+
b2 = 0x0
500+
b3 = 0x0
501+
b4 = 0x0
502+
b5 = 0x0
503+
t0 = zero(UInt64)
504+
t1 = zero(UInt64)
505+
506+
# Handle first iteration separately
507+
read = read_uint64_from_uint8_iter(iter)
508+
if read !== nothing
509+
l0, state, b0 = read
510+
# Repeat hashing chunks until a short read
511+
while true
512+
l1, state, b1 = read_uint64_from_uint8_iter(iter, state)
513+
if b1 == 0x8
514+
l2, state, b2 = read_uint64_from_uint8_iter(iter, state)
515+
if b2 == 0x8
516+
l3, state, b3 = read_uint64_from_uint8_iter(iter, state)
517+
if b3 == 0x8
518+
l4, state, b4 = read_uint64_from_uint8_iter(iter, state)
519+
if b4 == 0x8
520+
l5, state, b5 = read_uint64_from_uint8_iter(iter, state)
521+
if b5 == 0x8
522+
# Read start of next chunk
523+
read = read_uint64_from_uint8_iter(iter, state)
524+
if read[3] == 0x0
525+
# Read exactly 48 bytes
526+
t0 = l4
527+
t1 = l5
528+
break
529+
else
530+
# Read more than 48 bytes - process and continue to next chunk
531+
seed = hash_mix(l0 secret[1], l1 seed)
532+
see1 = hash_mix(l2 secret[2], l3 see1)
533+
see2 = hash_mix(l4 secret[3], l5 see2)
534+
buflen += 48
535+
l0, state, b0 = read
536+
b1 = 0
537+
b2 = 0
538+
b3 = 0
539+
b4 = 0
540+
b5 = 0
541+
if b0 < 8
542+
t0 = concat_shift(l4, l5, b0)
543+
t1 = concat_shift(l5, l0, b0)
544+
break
545+
end
546+
end
547+
else
548+
# Extract final 16 bytes at the first short read
549+
t0 = concat_shift(l3, l4, b5)
550+
t1 = concat_shift(l4, l5, b5)
551+
break
552+
end
553+
else
554+
t0 = concat_shift(l2, l3, b4)
555+
t1 = concat_shift(l3, l4, b4)
556+
break
557+
end
558+
else
559+
t0 = concat_shift(l1, l2, b3)
560+
t1 = concat_shift(l2, l3, b3)
561+
break
562+
end
563+
else
564+
t0 = concat_shift(l0, l1, b2)
565+
t1 = concat_shift(l1, l2, b2)
566+
break
567+
end
568+
else
569+
t0 = concat_shift(l5, l0, b1)
570+
t1 = concat_shift(l0, l1, b1)
571+
break
572+
end
573+
end
574+
end
575+
576+
# Partial chunk, handle based on size
577+
bytes_chunk = b0 + b1 + b2 + b3 + b4 + b5
578+
if buflen > 0
579+
# Finalize last full chunk
580+
seed ⊻= see1
581+
seed ⊻= see2
582+
end
583+
buflen += bytes_chunk
584+
if buflen 16
585+
if bytes_chunk 0x4
586+
seed ⊻= bytes_chunk
587+
if bytes_chunk 0x8
588+
a = l0
589+
b = t1
590+
else
591+
a = UInt64(l0 % UInt32)
592+
b = UInt64((l0 >>> ((0x8 * (bytes_chunk - 0x4)) % 0x3f)) % UInt32)
593+
end
594+
elseif bytes_chunk > 0x0
595+
b0 = l0 % UInt8
596+
b1 = (l0 >>> ((0x8 * div(bytes_chunk, 0x2)) % 0x3f)) % UInt8
597+
b2 = (l0 >>> ((0x8 * (bytes_chunk - 0x1)) % 0x3f)) % UInt8
598+
a = (UInt64(b0) << 45) | UInt64(b2)
599+
b = UInt64(b1)
600+
end
601+
else
602+
if bytes_chunk > 0x10
603+
seed = hash_mix(l0 secret[3], l1 seed)
604+
if bytes_chunk > 0x20
605+
seed = hash_mix(l2 secret[3], l3 seed)
606+
end
607+
end
608+
a = t0 bytes_chunk
609+
b = t1
610+
end
611+
612+
a = a secret[2]
613+
b = b seed
614+
b, a = mul_parts(a, b)
615+
return hash_mix(a secret[4], b secret[2] bytes_chunk)
616+
end
617+
347618
@assume_effects :total hash(data::String, h::UInt) =
348619
GC.@preserve data hash_bytes(pointer(data), sizeof(data), UInt64(h), HASH_SECRET) % UInt
349620

test/hashing.jl

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,22 @@ end
321321
src = only(code_typed(f, Tuple{UInt}))[1]
322322
@test count(stmt -> Meta.isexpr(stmt, :foreigncall), src.code) == 0
323323
end
324+
325+
@testset "hash_bytes consistency" begin
326+
# Test that hash_bytes(::Array), hash_bytes(Generator(identity, Array)), and hash_bytes(pointer(Array)) return the same values
327+
328+
for n in 0:1000
329+
b = rand(UInt8, n)
330+
a = Base.Generator(identity, b)
331+
332+
# Test hash_bytes(::Array) vs hash_bytes(pointer(Array))
333+
hash_array = Base.hash_bytes(b, UInt64(Base.HASH_SEED), Base.HASH_SECRET)
334+
hash_pointer = Base.hash_bytes(pointer(b), length(b), UInt64(Base.HASH_SEED), Base.HASH_SECRET)
335+
@test hash_array isa UInt64
336+
@test hash_array === hash_pointer
337+
338+
# Test hash_bytes(Generator(identity, Array)) vs hash_bytes(pointer(Array))
339+
hash_generator = Base.hash_bytes(a, UInt64(Base.HASH_SEED), Base.HASH_SECRET)
340+
@test hash_generator === hash_pointer
341+
end
342+
end

0 commit comments

Comments
 (0)