Skip to content

Commit e0c65bf

Browse files
committed
better implementation, distinguish int32 vs int64; add performance regression test
1 parent 5586db7 commit e0c65bf

File tree

5 files changed

+130
-21
lines changed

5 files changed

+130
-21
lines changed

compiler/vmops.nim

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ from md5 import getMD5
1818
from sighashes import symBodyDigest
1919
from times import cpuTime
2020

21-
from hashes import hash, hashBiggestInt
21+
from hashes import hash, hashUInt64, hashUInt32
2222

2323
template mathop(op) {.dirty.} =
2424
registerCallback(c, "stdlib.math." & astToStr(op), `op Wrapper`)
@@ -185,8 +185,10 @@ proc registerAdditionalOps*(c: PCtx) =
185185

186186
registerCallback c, "stdlib.hashes.hashVmImpl", hashVmImpl
187187

188-
registerCallback c, "stdlib.hashes.hashBiggestInt", proc (a: VmArgs) {.nimcall.} =
189-
a.setResult hashBiggestInt(getInt(a, 0))
188+
registerCallback c, "stdlib.hashes.hashUInt64", proc (a: VmArgs) {.nimcall.} =
189+
a.setResult hashUInt64(cast[uint64](getInt(a, 0)))
190+
registerCallback c, "stdlib.hashes.hashUInt32", proc (a: VmArgs) {.nimcall.} =
191+
a.setResult hashUInt32(cast[uint32](getInt(a, 0)))
190192

191193
proc hashVmImplByte(a: VmArgs) =
192194
# nkBracket[...]

lib/pure/hashes.nim

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,44 @@ proc hashData*(data: pointer, size: int): Hash =
8989
dec(s)
9090
result = !$h
9191

92-
proc hash*(x: string): Hash {.noSideEffect.}
92+
when defined(js):
93+
proc hash*(x: string): Hash {.noSideEffect.}
9394

94-
proc hashBiggestInt*(x: BiggestInt): Hash {.inline.} =
95+
proc hashUInt64*(x: uint64): Hash {.inline.} =
9596
## for internal use; user code should prefer `hash` overloads
9697
when nimvm: # in vmops
9798
doAssert false
9899
else:
99-
when defined(js):
100-
# could use BigInt, see https://github.com/nim-lang/RFCs/issues/187
101-
hash($x)
102-
else:
103-
hashData(cast[pointer](unsafeAddr x), type(x).sizeof)
100+
# would orders of magnitude worse, see thashes_perf toHighOrderBits
101+
# faster than using murmurhash or Jenkins via
102+
# hashData(cast[pointer](unsafeAddr x), type(x).sizeof)
103+
104+
# would a bit worse, see thashes_perf toInt64
105+
# type ByteArr = array[int64.sizeof, uint8]
106+
# result = murmurHash(cast[ptr ByteArr](unsafeAddr x)[])
107+
108+
# inspired from https://gist.github.com/badboy/6267743#64-bit-mix-functions
109+
var x = x
110+
x = (not x) + (x shl 21) # x = (x shl 21) - x - 1;
111+
x = x xor (x shr 24)
112+
x = (x + (x shl 3)) + (x shl 8) # x * 265
113+
x = x xor (x shr 14)
114+
x = (x + (x shl 2)) + (x shl 4) # x * 21
115+
x = x xor (x shr 28)
116+
x = x + (x shl 31)
117+
result = cast[Hash](x)
118+
119+
proc hashUInt32*(x: uint32): Hash {.inline.} =
120+
## for internal use; user code should prefer `hash` overloads
121+
# calling `hashUInt64(x)` would perform 1.736 worse, see thashes_perf toInt32
122+
when nimvm: # in vmops
123+
doAssert false
124+
else:
125+
# inspired from https://gist.github.com/badboy/6267743
126+
var x = x xor ((x shr 20) xor (x shr 12))
127+
# debugEcho ("hashUInt32", x)
128+
# return cast[Hash](x xor (x shr 7) xor (x shr 4))
129+
result = cast[Hash](x xor (x shr 7) xor (x shr 4))
104130

105131
proc hash*[T: SomeNumber | Ordinal | char](x: T): Hash {.inline.} =
106132
## Efficient hashing of numbers, ordinals (eg enum), char.
@@ -117,14 +143,16 @@ proc hash*[T: SomeNumber | Ordinal | char](x: T): Hash {.inline.} =
117143
# bugfix: the previous code was using `x = x + 1.0` (presumably for
118144
# handling negative 0), however this leads to collisions for small x due
119145
# to FP finite precision.
120-
let x = block:
121-
when sizeof(BiggestInt) == sizeof(T):
122-
cast[BiggestInt](x + T(0))
123-
else: # for nimvm
124-
cast[int32](x + T(0)).BiggestInt
146+
let x = x + T(0)
147+
when defined(js):
148+
when nimvm:
149+
# this could also be `hashUInt64(cast[uint64](x))` on a 32 bit machine,
150+
# but we can't query for int.sizeof since that's hardcoded for nim js
151+
hashUInt64(cast[uint64](x))
152+
else: hash($x.float)
125153
else:
126-
let x = x.BiggestInt
127-
hashBiggestInt(x)
154+
when sizeof(T) == sizeof(uint64): hashUInt64(cast[uint64](x))
155+
else: hashUInt32(cast[uint32](x))
128156
else:
129157
# empirically better for small types, the collision risk is limited anyway
130158
# due to cardinality of at most 2^16=65536

tests/collections/tcollections_to_string.nim

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import tables
77
import deques
88
import lists
99
import critbits
10+
import sequtils, algorithm
1011

1112
# Tests for tuples
1213
doAssert $(1, 2, 3) == "(1, 2, 3)"
@@ -26,9 +27,11 @@ doAssert $(toOrderedSet([1, 2, 3])) == "{1, 2, 3}"
2627
doAssert $(toOrderedSet(["1", "2", "3"])) == """{"1", "2", "3"}"""
2728
doAssert $(toOrderedSet(['1', '2', '3'])) == """{'1', '2', '3'}"""
2829

30+
proc toStrSorted(t: Table): string = $(toSeq(pairs(t)).sorted)
31+
2932
# Tests for tables
30-
doAssert $({1: "1", 2: "2"}.toTable) == """{1: "1", 2: "2"}"""
31-
doAssert $({"1": 1, "2": 2}.toTable) == """{"1": 1, "2": 2}"""
33+
doAssert {1: "1", 2: "2"}.toTable.toStrSorted == """@[(1, "1"), (2, "2")]"""
34+
doAssert {"1": 1, "2": 2}.toTable.toStrSorted == """@[("1", 1), ("2", 2)]"""
3235

3336
# Tests for deques
3437
block:

tests/collections/thashes_perf.nim

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
discard """
2+
cmd: "nim $target -d:danger -r $file"
3+
targets: "c cpp js"
4+
joinable: false
5+
timeout: 100.0
6+
"""
7+
8+
## performance regression tests, refs: #13393 and #11764
9+
## this takes into account insertion/retrieval time, plus time to compute hashes
10+
## in various input distributions to ensure we have both high quality hashes
11+
## (few collisions) as well as speedy hash computations.
12+
## We run this benchmark on: c, cpp, js, vm targets to ensure all cases are
13+
## covered.
14+
##
15+
## The spec timeout is a very generous upperbound; it's needed just in case
16+
## a regression causes CI to take forever on this test.
17+
18+
import times, tables
19+
20+
proc toInt64(a: int): int64 = cast[int64](a)
21+
proc toInt32(a: int): int32 = cast[int32](a)
22+
proc toHighOrderBits(a: int): uint64 =
23+
result = cast[uint64](a) shl 32
24+
25+
proc toSmallFloat(a: int): float =
26+
result = a.float*1e-20
27+
28+
# block ttables_perf:
29+
template fun() =
30+
proc TestHashIntInt[Fun](fun: Fun) =
31+
type T = type(fun(1))
32+
var tab = initTable[T, string]()
33+
const n = 100_000 # use that
34+
for i in 1..n:
35+
let h = fun(i)
36+
doAssert h notin tab
37+
tab[h] = $i
38+
doAssert tab[h] == $i
39+
40+
proc runTimingImpl[Fun](fun: Fun, name: string, timeout: float) =
41+
## return elapsed time in seconds
42+
type T = type(fun(1))
43+
echo "perf test begin: T=" & $T & " " & name
44+
45+
template epochTime2(): untyped =
46+
when nimvm: 0.0 # epochTime not defined in vm
47+
else: times.epochTime()
48+
49+
let eTime1 = epochTime2()
50+
51+
var numIter = 50
52+
when defined(js): numIter = 10
53+
when nimvm: numIter = 1
54+
else: discard
55+
56+
for i in 1 .. numIter:
57+
TestHashIntInt(fun)
58+
let eTime2 = epochTime2()
59+
let runtime = eTime2 - eTime1
60+
echo "pref test done: runtime: ", runtime, " timeout: ", timeout
61+
doAssert runtime < timeout # performance regression check, don't make it too tight for CI
62+
63+
template runTiming(fun, timout) = runTimingImpl(fun, astToStr(fun), timout)
64+
# leaving some large buffer for timing, but not too much to catch performance regressions
65+
let timeouts = when defined(js): [8.0, 8.0, 8.0] else: [4.0, 3.0, 5.0]
66+
# on my local OSX machine I get: [1.44, 0.85, 1.2316, 1.38] for `nim c` and ~1.8 for `nim js`
67+
# azure CI OSX I get: [2.8, 1.5, 2.98] for `nim c`
68+
69+
runTiming toInt64, timeouts[0]
70+
runTiming toInt32, timeouts[1]
71+
runTiming toHighOrderBits, timeouts[2]
72+
when false: # disabled to save CI time but easy to turn back on for deubugging
73+
runTiming toSmallFloat, 3.0
74+
75+
when defined(c) or defined(js): # skip cpp (a bit redundant with c in this case)
76+
static: fun()
77+
fun()

tests/collections/ttables.nim

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ block tableconstr:
164164

165165
block ttables2:
166166
proc TestHashIntInt() =
167-
var tab = initTable[int,int]()
167+
var tab = initTable[int, int]()
168168
const n = 1_000_000 # bottleneck: 50 seconds on OSX in debug mode
169169
for i in 1..n:
170170
tab[i] = i
@@ -185,7 +185,6 @@ block ttables2:
185185
delTab.del(i)
186186
delTab[5] = 5
187187

188-
189188
run1()
190189
echo "2"
191190

0 commit comments

Comments
 (0)