Skip to content

Commit 48a8bf1

Browse files
authored
add safer, more versatile version of rapidhash for any array and for any iterable (#59185)
Tested that array gives the same answer on random data, and that performance is equivalent (with LLVM 20 on AArch64). Neither of these methods is currently used for anything, but the intent is to change some users (e.g. String) to the array interface and other users (e.g. large Numbers and AbstractString) to the iterator interface (I already have both written–or rather Claude does–but in a different repo right now). The iterator interface does not exist anywhere else, since it is about 8x slower (essentially operating byte by byte instead of the intended 64-bit chunks). ```julia 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) ``` PR implemented mostly by Claude, though I had to fix its math in several places since it got quite confused by the logical nesting of if branches and the implications of extracting trailing bytes. Test "proof" of correctness: ``` julia> @time for n = 0:10000 b = codeunits(randstring(n)) a = Base.Generator(identity, b) h1 = Base.hash_bytes(a, Base.HASH_SEED, Base.HASH_SECRET) h2 = Base.hash_bytes(b, Base.HASH_SEED, Base.HASH_SECRET) h3 = Base.hash_bytes(pointer(b), length(b), Base.HASH_SEED, Base.HASH_SECRET) @Assert h1 == h2 == h3 end 0.174760 seconds (10.00 k allocations: 48.855 MiB, 3.97% gc time) ```
2 parents 66c737e + a23ff47 commit 48a8bf1

File tree

5 files changed

+299
-13
lines changed

5 files changed

+299
-13
lines changed

base/Base.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ include("subarray.jl")
3838
include("views.jl")
3939

4040
# numeric operations
41-
include("hashing.jl")
4241
include("div.jl")
4342
include("twiceprecision.jl")
4443
include("complex.jl")
@@ -89,6 +88,9 @@ include("strings/string.jl")
8988
include("strings/substring.jl")
9089
include("strings/cstring.jl")
9190

91+
include("cartesian.jl")
92+
using .Cartesian
93+
include("hashing.jl")
9294
include("osutils.jl")
9395

9496
# Core I/O
@@ -115,8 +117,6 @@ include("arrayshow.jl")
115117
include("methodshow.jl")
116118

117119
# multidimensional arrays
118-
include("cartesian.jl")
119-
using .Cartesian
120120
include("multidimensional.jl")
121121

122122
include("broadcast.jl")

base/cartesian.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,9 +395,9 @@ end
395395

396396
## Resolve expressions at parsing time ##
397397

398-
const exprresolve_arith_dict = Dict{Symbol,Function}(:+ => +,
398+
const exprresolve_arith_dict = IdDict{Symbol,Function}(:+ => +,
399399
:- => -, :* => *, :/ => /, :^ => ^, :div => div)
400-
const exprresolve_cond_dict = Dict{Symbol,Function}(:(==) => ==,
400+
const exprresolve_cond_dict = IdDict{Symbol,Function}(:(==) => ==,
401401
:(<) => <, :(>) => >, :(<=) => <=, :(>=) => >=)
402402

403403
function exprresolve_arith(ex::Expr)

base/complex.jl

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,8 @@ isequal(z::Real, w::Complex) = isequal(z,real(w))::Bool & isequal(zero(z),imag(w
248248

249249
in(x::Complex, r::AbstractRange{<:Real}) = isreal(x) && real(x) in r
250250

251-
if UInt === UInt64
252-
const h_imag = 0x32a7a07f3e7cd1f9
253-
else
254-
const h_imag = 0x3e7cd1f9
255-
end
256-
const hash_0_imag = hash(0, h_imag)
251+
const h_imag = 0x32a7a07f3e7cd1f % UInt
252+
const hash_0_imag = 0x153e9f914f9b5b92 % UInt
257253

258254
function hash(z::Complex, h::UInt)
259255
# TODO: with default argument specialization, this would be better:

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)