LockFreeHash.nim 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. #nim c -t:-march=i686 --cpu:amd64 --threads:on -d:release lockfreehash.nim
  2. import math, hashes
  3. #------------------------------------------------------------------------------
  4. ## Memory Utility Functions
  5. proc newHeap*[T](): ptr T =
  6. result = cast[ptr T](alloc0(sizeof(T)))
  7. proc copyNew*[T](x: var T): ptr T =
  8. var
  9. size = sizeof(T)
  10. mem = alloc(size)
  11. copyMem(mem, x.addr, size)
  12. return cast[ptr T](mem)
  13. proc copyTo*[T](val: var T, dest: int) =
  14. copyMem(pointer(dest), val.addr, sizeof(T))
  15. proc allocType*[T](): pointer = alloc(sizeof(T))
  16. proc newShared*[T](): ptr T =
  17. result = cast[ptr T](allocShared0(sizeof(T)))
  18. proc copyShared*[T](x: var T): ptr T =
  19. var
  20. size = sizeof(T)
  21. mem = allocShared(size)
  22. copyMem(mem, x.addr, size)
  23. return cast[ptr T](mem)
  24. #------------------------------------------------------------------------------
  25. ## Pointer arithmetic
  26. proc `+`*(p: pointer, i: int): pointer {.inline.} =
  27. cast[pointer](cast[int](p) + i)
  28. const
  29. minTableSize = 8
  30. reProbeLimit = 12
  31. minCopyWork = 4096
  32. intSize = sizeof(int)
  33. when sizeof(int) == 4: # 32bit
  34. type
  35. Raw = range[0..1073741823]
  36. ## The range of uint values that can be stored directly in a value slot
  37. ## when on a 32 bit platform
  38. {.deprecated: [TRaw: Raw].}
  39. elif sizeof(int) == 8: # 64bit
  40. type
  41. Raw = range[0'i64..4611686018427387903'i64]
  42. ## The range of uint values that can be stored directly in a value slot
  43. ## when on a 64 bit platform
  44. {.deprecated: [TRaw: Raw].}
  45. else:
  46. {.error: "unsupported platform".}
  47. type
  48. Entry = tuple
  49. key: int
  50. value: int
  51. EntryArr = ptr array[0..10_000_000, Entry]
  52. PConcTable[K,V] = ptr object {.pure.}
  53. len: int
  54. used: int
  55. active: int
  56. copyIdx: int
  57. copyDone: int
  58. next: PConcTable[K,V]
  59. data: EntryArr
  60. {.deprecated: [TEntry: Entry, TEntryArr: EntryArr].}
  61. proc setVal[K,V](table: var PConcTable[K,V], key: int, val: int,
  62. expVal: int, match: bool): int
  63. #------------------------------------------------------------------------------
  64. # Create a new table
  65. proc newLFTable*[K,V](size: int = minTableSize): PConcTable[K,V] =
  66. let
  67. dataLen = max(nextPowerOfTwo(size), minTableSize)
  68. dataSize = dataLen*sizeof(Entry)
  69. dataMem = allocShared0(dataSize)
  70. tableSize = 7 * intSize
  71. tableMem = allocShared0(tableSize)
  72. table = cast[PConcTable[K,V]](tableMem)
  73. table.len = dataLen
  74. table.used = 0
  75. table.active = 0
  76. table.copyIdx = 0
  77. table.copyDone = 0
  78. table.next = nil
  79. table.data = cast[EntryArr](dataMem)
  80. result = table
  81. #------------------------------------------------------------------------------
  82. # Delete a table
  83. proc deleteConcTable[K,V](tbl: PConcTable[K,V]) =
  84. deallocShared(tbl.data)
  85. deallocShared(tbl)
  86. #------------------------------------------------------------------------------
  87. proc `[]`[K,V](table: var PConcTable[K,V], i: int): var Entry {.inline.} =
  88. table.data[i]
  89. #------------------------------------------------------------------------------
  90. # State flags stored in ptr
  91. proc pack[T](x: T): int {.inline.} =
  92. result = (cast[int](x) shl 2)
  93. #echo("packKey ",cast[int](x) , " -> ", result)
  94. # Pop the flags off returning a 4 byte aligned ptr to our Key or Val
  95. proc pop(x: int): int {.inline.} =
  96. result = x and 0xFFFFFFFC'i32
  97. # Pop the raw value off of our Key or Val
  98. proc popRaw(x: int): int {.inline.} =
  99. result = x shr 2
  100. # Pop the flags off returning a 4 byte aligned ptr to our Key or Val
  101. proc popPtr[V](x: int): ptr V {.inline.} =
  102. result = cast[ptr V](pop(x))
  103. #echo("popPtr " & $x & " -> " & $cast[int](result))
  104. # Ghost (sentinel)
  105. # K or V is no longer valid use new table
  106. const Ghost = 0xFFFFFFFC
  107. proc isGhost(x: int): bool {.inline.} =
  108. result = x == 0xFFFFFFFC
  109. # Tombstone
  110. # applied to V = K is dead
  111. proc isTomb(x: int): bool {.inline.} =
  112. result = (x and 0x00000002) != 0
  113. proc setTomb(x: int): int {.inline.} =
  114. result = x or 0x00000002
  115. # Prime
  116. # K or V is in new table copied from old
  117. proc isPrime(x: int): bool {.inline.} =
  118. result = (x and 0x00000001) != 0
  119. proc setPrime(x: int): int {.inline.} =
  120. result = x or 0x00000001
  121. #------------------------------------------------------------------------------
  122. ##This is for i32 only need to override for i64
  123. proc hashInt(x: int):int {.inline.} =
  124. var h = uint32(x) #shr 2'u32
  125. h = h xor (h shr 16'u32)
  126. h *= 0x85ebca6b'u32
  127. h = h xor (h shr 13'u32)
  128. h *= 0xc2b2ae35'u32
  129. h = h xor (h shr 16'u32)
  130. result = int(h)
  131. #------------------------------------------------------------------------------
  132. proc resize[K,V](self: PConcTable[K,V]): PConcTable[K,V] =
  133. var next = atomic_load_n(self.next.addr, ATOMIC_RELAXED)
  134. #echo("next = " & $cast[int](next))
  135. if next != nil:
  136. #echo("A new table already exists, copy in progress")
  137. return next
  138. var
  139. oldLen = atomic_load_n(self.len.addr, ATOMIC_RELAXED)
  140. newTable = newLFTable[K,V](oldLen*2)
  141. success = atomic_compare_exchange_n(self.next.addr, next.addr, newTable,
  142. false, ATOMIC_RELAXED, ATOMIC_RELAXED)
  143. if not success:
  144. echo("someone beat us to it! delete table we just created and return his " & $cast[int](next))
  145. deleteConcTable(newTable)
  146. return next
  147. else:
  148. echo("Created New Table! " & $cast[int](newTable) & " Size = " & $newTable.len)
  149. return newTable
  150. #------------------------------------------------------------------------------
  151. #proc keyEQ[K](key1: ptr K, key2: ptr K): bool {.inline.} =
  152. proc keyEQ[K](key1: int, key2: int): bool {.inline.} =
  153. result = false
  154. when K is Raw:
  155. if key1 == key2:
  156. result = true
  157. else:
  158. var
  159. p1 = popPtr[K](key1)
  160. p2 = popPtr[K](key2)
  161. if p1 != nil and p2 != nil:
  162. if cast[int](p1) == cast[int](p2):
  163. return true
  164. if p1[] == p2[]:
  165. return true
  166. #------------------------------------------------------------------------------
  167. #proc tableFull(self: var PConcTable[K,V]) : bool {.inline.} =
  168. #------------------------------------------------------------------------------
  169. proc copySlot[K,V](idx: int, oldTbl: var PConcTable[K,V], newTbl: var PConcTable[K,V]): bool =
  170. #echo("Copy idx " & $idx)
  171. var
  172. oldVal = 0
  173. oldkey = 0
  174. ok = false
  175. result = false
  176. #Block the key so no other threads waste time here
  177. while not ok:
  178. ok = atomic_compare_exchange_n(oldTbl[idx].key.addr, oldKey.addr,
  179. setTomb(oldKey), false, ATOMIC_RELAXED, ATOMIC_RELAXED)
  180. #echo("oldKey was = " & $oldKey & " set it to tomb " & $setTomb(oldKey))
  181. #Prevent new values from appearing in the old table by priming
  182. oldVal = atomic_load_n(oldTbl[idx].value.addr, ATOMIC_RELAXED)
  183. while not isPrime(oldVal):
  184. var box = if oldVal == 0 or isTomb(oldVal) : oldVal.setTomb.setPrime
  185. else: oldVal.setPrime
  186. if atomic_compare_exchange_n(oldTbl[idx].value.addr, oldVal.addr,
  187. box, false, ATOMIC_RELAXED, ATOMIC_RELAXED):
  188. if isPrime(box) and isTomb(box):
  189. return true
  190. oldVal = box
  191. break
  192. #echo("oldVal was = ", oldVal, " set it to prime ", box)
  193. if isPrime(oldVal) and isTomb(oldVal):
  194. #when not (K is Raw):
  195. # deallocShared(popPtr[K](oldKey))
  196. return false
  197. if isTomb(oldVal):
  198. echo("oldVal is Tomb!!!, should not happen")
  199. if pop(oldVal) != 0:
  200. result = setVal(newTbl, pop(oldKey), pop(oldVal), 0, true) == 0
  201. #if result:
  202. #echo("Copied a Slot! idx= " & $idx & " key= " & $oldKey & " val= " & $oldVal)
  203. #else:
  204. #echo("copy slot failed")
  205. # Our copy is done so we disable the old slot
  206. while not ok:
  207. ok = atomic_compare_exchange_n(oldTbl[idx].value.addr, oldVal.addr,
  208. oldVal.setTomb.setPrime , false, ATOMIC_RELAXED, ATOMIC_RELAXED)
  209. #echo("disabled old slot")
  210. #echo"---------------------"
  211. #------------------------------------------------------------------------------
  212. proc promote[K,V](table: var PConcTable[K,V]) =
  213. var
  214. newData = atomic_load_n(table.next.data.addr, ATOMIC_RELAXED)
  215. newLen = atomic_load_n(table.next.len.addr, ATOMIC_RELAXED)
  216. newUsed = atomic_load_n(table.next.used.addr, ATOMIC_RELAXED)
  217. deallocShared(table.data)
  218. atomic_store_n(table.data.addr, newData, ATOMIC_RELAXED)
  219. atomic_store_n(table.len.addr, newLen, ATOMIC_RELAXED)
  220. atomic_store_n(table.used.addr, newUsed, ATOMIC_RELAXED)
  221. atomic_store_n(table.copyIdx.addr, 0, ATOMIC_RELAXED)
  222. atomic_store_n(table.copyDone.addr, 0, ATOMIC_RELAXED)
  223. deallocShared(table.next)
  224. atomic_store_n(table.next.addr, nil, ATOMIC_RELAXED)
  225. echo("new table swapped!")
  226. #------------------------------------------------------------------------------
  227. proc checkAndPromote[K,V](table: var PConcTable[K,V], workDone: int): bool =
  228. var
  229. oldLen = atomic_load_n(table.len.addr, ATOMIC_RELAXED)
  230. copyDone = atomic_load_n(table.copyDone.addr, ATOMIC_RELAXED)
  231. ok: bool
  232. result = false
  233. if workDone > 0:
  234. #echo("len to copy =" & $oldLen)
  235. #echo("copyDone + workDone = " & $copyDone & " + " & $workDone)
  236. while not ok:
  237. ok = atomic_compare_exchange_n(table.copyDone.addr, copyDone.addr,
  238. copyDone + workDone, false, ATOMIC_RELAXED, ATOMIC_RELAXED)
  239. #if ok: echo("set copyDone")
  240. # If the copy is done we can promote this table
  241. if copyDone + workDone >= oldLen:
  242. # Swap new data
  243. #echo("work is done!")
  244. table.promote
  245. result = true
  246. #------------------------------------------------------------------------------
  247. proc copySlotAndCheck[K,V](table: var PConcTable[K,V], idx: int):
  248. PConcTable[K,V] =
  249. var
  250. newTable = cast[PConcTable[K,V]](atomic_load_n(table.next.addr, ATOMIC_RELAXED))
  251. result = newTable
  252. if newTable != nil and copySlot(idx, table, newTable):
  253. #echo("copied a single slot, idx = " & $idx)
  254. if checkAndPromote(table, 1): return table
  255. #------------------------------------------------------------------------------
  256. proc helpCopy[K,V](table: var PConcTable[K,V]): PConcTable[K,V] =
  257. var
  258. newTable = cast[PConcTable[K,V]](atomic_load_n(table.next.addr, ATOMIC_RELAXED))
  259. result = newTable
  260. if newTable != nil:
  261. var
  262. oldLen = atomic_load_n(table.len.addr, ATOMIC_RELAXED)
  263. copyDone = atomic_load_n(table.copyDone.addr, ATOMIC_RELAXED)
  264. copyIdx = 0
  265. work = min(oldLen, minCopyWork)
  266. #panicStart = -1
  267. workDone = 0
  268. if copyDone < oldLen:
  269. var ok: bool
  270. while not ok:
  271. ok = atomic_compare_exchange_n(table.copyIdx.addr, copyIdx.addr,
  272. copyIdx + work, false, ATOMIC_RELAXED, ATOMIC_RELAXED)
  273. #echo("copy idx = ", copyIdx)
  274. for i in 0..work-1:
  275. var idx = (copyIdx + i) and (oldLen - 1)
  276. if copySlot(idx, table, newTable):
  277. workDone += 1
  278. if workDone > 0:
  279. #echo("did work ", workDone, " on thread ", cast[int](myThreadID[pointer]()))
  280. if checkAndPromote(table, workDone): return table
  281. # In case a thread finished all the work then got stalled before promotion
  282. if checkAndPromote(table, 0): return table
  283. #------------------------------------------------------------------------------
  284. proc setVal[K,V](table: var PConcTable[K,V], key: int, val: int,
  285. expVal: int, match: bool): int =
  286. #echo("-try set- in table ", " key = ", (popPtr[K](key)[]), " val = ", val)
  287. when K is Raw:
  288. var idx = hashInt(key)
  289. else:
  290. var idx = popPtr[K](key)[].hash
  291. var
  292. nextTable: PConcTable[K,V]
  293. probes = 1
  294. # spin until we find a key slot or build and jump to next table
  295. while true:
  296. idx = idx and (table.len - 1)
  297. #echo("try set idx = " & $idx & "for" & $key)
  298. var
  299. probedKey = 0
  300. openKey = atomic_compare_exchange_n(table[idx].key.addr, probedKey.addr,
  301. key, false, ATOMIC_RELAXED, ATOMIC_RELAXED)
  302. if openKey:
  303. if val.isTomb:
  304. #echo("val was tomb, bail, no reason to set an open slot to tomb")
  305. return val
  306. #increment used slots
  307. #echo("found an open slot, total used = " &
  308. #$atomic_add_fetch(table.used.addr, 1, ATOMIC_RELAXED))
  309. discard atomic_add_fetch(table.used.addr, 1, ATOMIC_RELAXED)
  310. break # We found an open slot
  311. #echo("set idx ", idx, " key = ", key, " probed = ", probedKey)
  312. if keyEQ[K](probedKey, key):
  313. #echo("we found the matching slot")
  314. break # We found a matching slot
  315. if (not(expVal != 0 and match)) and (probes >= reProbeLimit or key.isTomb):
  316. if key.isTomb: echo("Key is Tombstone")
  317. #if probes >= reProbeLimit: echo("Too much probing " & $probes)
  318. #echo("try to resize")
  319. #create next bigger table
  320. nextTable = resize(table)
  321. #help do some copying
  322. #echo("help copy old table to new")
  323. nextTable = helpCopy(table)
  324. #now setVal in the new table instead
  325. #echo("jumping to next table to set val")
  326. return setVal(nextTable, key, val, expVal, match)
  327. else:
  328. idx += 1
  329. probes += 1
  330. # Done spinning for a new slot
  331. var oldVal = atomic_load_n(table[idx].value.addr, ATOMIC_RELAXED)
  332. if val == oldVal:
  333. #echo("this val is alredy in the slot")
  334. return oldVal
  335. nextTable = atomic_load_n(table.next.addr, ATOMIC_SEQ_CST)
  336. if nextTable == nil and
  337. ((oldVal == 0 and
  338. (probes >= reProbeLimit or table.used / table.len > 0.8)) or
  339. (isPrime(oldVal))):
  340. if table.used / table.len > 0.8: echo("resize because usage ratio = " &
  341. $(table.used / table.len))
  342. if isPrime(oldVal): echo("old val isPrime, should be a rare mem ordering event")
  343. nextTable = resize(table)
  344. if nextTable != nil:
  345. #echo("tomb old slot then set in new table")
  346. nextTable = copySlotAndCheck(table,idx)
  347. return setVal(nextTable, key, val, expVal, match)
  348. # Finally ready to add new val to table
  349. while true:
  350. if match and oldVal != expVal:
  351. #echo("set failed, no match oldVal= " & $oldVal & " expVal= " & $expVal)
  352. return oldVal
  353. if atomic_compare_exchange_n(table[idx].value.addr, oldVal.addr,
  354. val, false, ATOMIC_RELEASE, ATOMIC_RELAXED):
  355. #echo("val set at table " & $cast[int](table))
  356. if expVal != 0:
  357. if (oldVal == 0 or isTomb(oldVal)) and not isTomb(val):
  358. discard atomic_add_fetch(table.active.addr, 1, ATOMIC_RELAXED)
  359. elif not (oldVal == 0 or isTomb(oldVal)) and isTomb(val):
  360. discard atomic_add_fetch(table.active.addr, -1, ATOMIC_RELAXED)
  361. if oldVal == 0 and expVal != 0:
  362. return setTomb(oldVal)
  363. else: return oldVal
  364. if isPrime(oldVal):
  365. nextTable = copySlotAndCheck(table, idx)
  366. return setVal(nextTable, key, val, expVal, match)
  367. #------------------------------------------------------------------------------
  368. proc getVal[K,V](table: var PConcTable[K,V], key: int): int =
  369. #echo("-try get- key = " & $key)
  370. when K is Raw:
  371. var idx = hashInt(key)
  372. else:
  373. var idx = popPtr[K](key)[].hash
  374. #echo("get idx ", idx)
  375. var
  376. probes = 0
  377. val: int
  378. while true:
  379. idx = idx and (table.len - 1)
  380. var
  381. newTable: PConcTable[K,V] # = atomic_load_n(table.next.addr, ATOMIC_ACQUIRE)
  382. probedKey = atomic_load_n(table[idx].key.addr, ATOMIC_SEQ_CST)
  383. if keyEQ[K](probedKey, key):
  384. #echo("found key after ", probes+1)
  385. val = atomic_load_n(table[idx].value.addr, ATOMIC_ACQUIRE)
  386. if not isPrime(val):
  387. if isTomb(val):
  388. #echo("val was tomb but not prime")
  389. return 0
  390. else:
  391. #echo("-GotIt- idx = ", idx, " key = ", key, " val ", val )
  392. return val
  393. else:
  394. newTable = copySlotAndCheck(table, idx)
  395. return getVal(newTable, key)
  396. else:
  397. #echo("probe ", probes, " idx = ", idx, " key = ", key, " found ", probedKey )
  398. if probes >= reProbeLimit*4 or key.isTomb:
  399. if newTable == nil:
  400. #echo("too many probes and no new table ", key, " ", idx )
  401. return 0
  402. else:
  403. newTable = helpCopy(table)
  404. return getVal(newTable, key)
  405. idx += 1
  406. probes += 1
  407. #------------------------------------------------------------------------------
  408. #proc set*(table: var PConcTable[Raw,Raw], key: Raw, val: Raw) =
  409. # discard setVal(table, pack(key), pack(key), 0, false)
  410. #proc set*[V](table: var PConcTable[Raw,V], key: Raw, val: ptr V) =
  411. # discard setVal(table, pack(key), cast[int](val), 0, false)
  412. proc set*[K,V](table: var PConcTable[K,V], key: var K, val: var V) =
  413. when not (K is Raw):
  414. var newKey = cast[int](copyShared(key))
  415. else:
  416. var newKey = pack(key)
  417. when not (V is Raw):
  418. var newVal = cast[int](copyShared(val))
  419. else:
  420. var newVal = pack(val)
  421. var oldPtr = pop(setVal(table, newKey, newVal, 0, false))
  422. #echo("oldPtr = ", cast[int](oldPtr), " newPtr = ", cast[int](newPtr))
  423. when not (V is Raw):
  424. if newVal != oldPtr and oldPtr != 0:
  425. deallocShared(cast[ptr V](oldPtr))
  426. proc get*[K,V](table: var PConcTable[K,V], key: var K): V =
  427. when not (V is Raw):
  428. when not (K is Raw):
  429. return popPtr[V](getVal(table, cast[int](key.addr)))[]
  430. else:
  431. return popPtr[V](getVal(table, pack(key)))[]
  432. else:
  433. when not (K is Raw):
  434. return popRaw(getVal(table, cast[int](key.addr)))
  435. else:
  436. return popRaw(getVal(table, pack(key)))
  437. #proc `[]`[K,V](table: var PConcTable[K,V], key: K): PEntry[K,V] {.inline.} =
  438. # getVal(table, key)
  439. #proc `[]=`[K,V](table: var PConcTable[K,V], key: K, val: V): PEntry[K,V] {.inline.} =
  440. # setVal(table, key, val)
  441. #Tests ----------------------------
  442. when not defined(testing) and isMainModule:
  443. import locks, times, mersenne
  444. const
  445. numTests = 100000
  446. numThreads = 10
  447. type
  448. TestObj = tuple
  449. thr: int
  450. f0: int
  451. f1: int
  452. Data = tuple[k: string,v: TestObj]
  453. PDataArr = array[0..numTests-1, Data]
  454. Dict = PConcTable[string,TestObj]
  455. {.deprecated: [TTestObj: TestObj, TData: Data].}
  456. var
  457. thr: array[0..numThreads-1, Thread[Dict]]
  458. table = newLFTable[string,TestObj](8)
  459. rand = newMersenneTwister(2525)
  460. proc createSampleData(len: int): PDataArr =
  461. #result = cast[PDataArr](allocShared0(sizeof(Data)*numTests))
  462. for i in 0..len-1:
  463. result[i].k = "mark" & $(i+1)
  464. #echo("mark" & $(i+1), " ", hash("mark" & $(i+1)))
  465. result[i].v.thr = 0
  466. result[i].v.f0 = i+1
  467. result[i].v.f1 = 0
  468. #echo("key = " & $(i+1) & " Val ptr = " & $cast[int](result[i].v.addr))
  469. proc threadProc(tp: Dict) {.thread.} =
  470. var t = cpuTime();
  471. for i in 1..numTests:
  472. var key = "mark" & $(i)
  473. var got = table.get(key)
  474. got.thr = cast[int](myThreadID[pointer]())
  475. got.f1 = got.f1 + 1
  476. table.set(key, got)
  477. t = cpuTime() - t
  478. echo t
  479. var testData = createSampleData(numTests)
  480. for i in 0..numTests-1:
  481. table.set(testData[i].k, testData[i].v)
  482. var i = 0
  483. while i < numThreads:
  484. createThread(thr[i], threadProc, table)
  485. i += 1
  486. joinThreads(thr)
  487. var fails = 0
  488. for i in 0..numTests-1:
  489. var got = table.get(testData[i].k)
  490. if got.f0 != i+1 or got.f1 != numThreads:
  491. fails += 1
  492. echo(got)
  493. echo("Failed read or write = ", fails)
  494. #for i in 1..numTests:
  495. # echo(i, " = ", hashInt(i) and 8191)
  496. deleteConcTable(table)