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