@nxtedition/rocksdb 8.2.8 → 9.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (483) hide show
  1. package/binding.cc +0 -21
  2. package/deps/rocksdb/rocksdb/CMakeLists.txt +20 -10
  3. package/deps/rocksdb/rocksdb/Makefile +37 -25
  4. package/deps/rocksdb/rocksdb/README.md +29 -0
  5. package/deps/rocksdb/rocksdb/TARGETS +25 -2
  6. package/deps/rocksdb/rocksdb/cache/cache.cc +35 -0
  7. package/deps/rocksdb/rocksdb/cache/cache_bench_tool.cc +229 -74
  8. package/deps/rocksdb/rocksdb/cache/cache_helpers.cc +2 -1
  9. package/deps/rocksdb/rocksdb/cache/cache_reservation_manager.h +4 -3
  10. package/deps/rocksdb/rocksdb/cache/cache_test.cc +58 -95
  11. package/deps/rocksdb/rocksdb/cache/charged_cache.cc +4 -2
  12. package/deps/rocksdb/rocksdb/cache/charged_cache.h +5 -3
  13. package/deps/rocksdb/rocksdb/cache/clock_cache.cc +2683 -496
  14. package/deps/rocksdb/rocksdb/cache/clock_cache.h +580 -159
  15. package/deps/rocksdb/rocksdb/cache/compressed_secondary_cache.cc +145 -42
  16. package/deps/rocksdb/rocksdb/cache/compressed_secondary_cache.h +20 -1
  17. package/deps/rocksdb/rocksdb/cache/compressed_secondary_cache_test.cc +391 -17
  18. package/deps/rocksdb/rocksdb/cache/lru_cache.cc +7 -5
  19. package/deps/rocksdb/rocksdb/cache/lru_cache_test.cc +309 -212
  20. package/deps/rocksdb/rocksdb/cache/secondary_cache.cc +0 -32
  21. package/deps/rocksdb/rocksdb/cache/secondary_cache_adapter.cc +439 -12
  22. package/deps/rocksdb/rocksdb/cache/secondary_cache_adapter.h +44 -2
  23. package/deps/rocksdb/rocksdb/cache/sharded_cache.cc +11 -1
  24. package/deps/rocksdb/rocksdb/cache/sharded_cache.h +16 -3
  25. package/deps/rocksdb/rocksdb/cache/tiered_secondary_cache.cc +119 -0
  26. package/deps/rocksdb/rocksdb/cache/tiered_secondary_cache.h +155 -0
  27. package/deps/rocksdb/rocksdb/cache/tiered_secondary_cache_test.cc +711 -0
  28. package/deps/rocksdb/rocksdb/cache/typed_cache.h +17 -11
  29. package/deps/rocksdb/rocksdb/crash_test.mk +14 -0
  30. package/deps/rocksdb/rocksdb/db/arena_wrapped_db_iter.cc +28 -12
  31. package/deps/rocksdb/rocksdb/db/arena_wrapped_db_iter.h +1 -0
  32. package/deps/rocksdb/rocksdb/db/blob/blob_contents.h +2 -1
  33. package/deps/rocksdb/rocksdb/db/blob/blob_file_builder.cc +1 -1
  34. package/deps/rocksdb/rocksdb/db/blob/blob_file_builder_test.cc +1 -1
  35. package/deps/rocksdb/rocksdb/db/blob/blob_file_cache.cc +2 -2
  36. package/deps/rocksdb/rocksdb/db/blob/blob_file_cache.h +1 -1
  37. package/deps/rocksdb/rocksdb/db/blob/blob_file_reader.cc +20 -22
  38. package/deps/rocksdb/rocksdb/db/blob/blob_file_reader.h +1 -2
  39. package/deps/rocksdb/rocksdb/db/blob/blob_file_reader_test.cc +1 -1
  40. package/deps/rocksdb/rocksdb/db/blob/blob_log_sequential_reader.cc +2 -3
  41. package/deps/rocksdb/rocksdb/db/blob/blob_source_test.cc +1 -1
  42. package/deps/rocksdb/rocksdb/db/blob/db_blob_basic_test.cc +8 -0
  43. package/deps/rocksdb/rocksdb/db/blob/db_blob_index_test.cc +7 -3
  44. package/deps/rocksdb/rocksdb/db/builder.cc +35 -10
  45. package/deps/rocksdb/rocksdb/db/c.cc +233 -6
  46. package/deps/rocksdb/rocksdb/db/c_test.c +140 -6
  47. package/deps/rocksdb/rocksdb/db/column_family.cc +110 -51
  48. package/deps/rocksdb/rocksdb/db/column_family.h +34 -2
  49. package/deps/rocksdb/rocksdb/db/column_family_test.cc +314 -7
  50. package/deps/rocksdb/rocksdb/db/compact_files_test.cc +4 -1
  51. package/deps/rocksdb/rocksdb/db/compaction/compaction.cc +106 -23
  52. package/deps/rocksdb/rocksdb/db/compaction/compaction.h +47 -9
  53. package/deps/rocksdb/rocksdb/db/compaction/compaction_iterator.cc +10 -11
  54. package/deps/rocksdb/rocksdb/db/compaction/compaction_iterator.h +17 -6
  55. package/deps/rocksdb/rocksdb/db/compaction/compaction_iterator_test.cc +2 -2
  56. package/deps/rocksdb/rocksdb/db/compaction/compaction_job.cc +148 -60
  57. package/deps/rocksdb/rocksdb/db/compaction/compaction_job.h +22 -7
  58. package/deps/rocksdb/rocksdb/db/compaction/compaction_job_stats_test.cc +2 -0
  59. package/deps/rocksdb/rocksdb/db/compaction/compaction_job_test.cc +8 -4
  60. package/deps/rocksdb/rocksdb/db/compaction/compaction_outputs.cc +33 -23
  61. package/deps/rocksdb/rocksdb/db/compaction/compaction_outputs.h +14 -5
  62. package/deps/rocksdb/rocksdb/db/compaction/compaction_picker.cc +11 -11
  63. package/deps/rocksdb/rocksdb/db/compaction/compaction_picker_fifo.cc +3 -0
  64. package/deps/rocksdb/rocksdb/db/compaction/compaction_picker_test.cc +90 -4
  65. package/deps/rocksdb/rocksdb/db/compaction/compaction_picker_universal.cc +170 -95
  66. package/deps/rocksdb/rocksdb/db/compaction/file_pri.h +3 -1
  67. package/deps/rocksdb/rocksdb/db/compaction/tiered_compaction_test.cc +32 -58
  68. package/deps/rocksdb/rocksdb/db/comparator_db_test.cc +3 -1
  69. package/deps/rocksdb/rocksdb/db/convenience.cc +20 -3
  70. package/deps/rocksdb/rocksdb/db/convenience_impl.h +15 -0
  71. package/deps/rocksdb/rocksdb/db/corruption_test.cc +17 -0
  72. package/deps/rocksdb/rocksdb/db/cuckoo_table_db_test.cc +1 -0
  73. package/deps/rocksdb/rocksdb/db/db_basic_test.cc +46 -10
  74. package/deps/rocksdb/rocksdb/db/db_block_cache_test.cc +13 -3
  75. package/deps/rocksdb/rocksdb/db/db_bloom_filter_test.cc +74 -15
  76. package/deps/rocksdb/rocksdb/db/db_compaction_filter_test.cc +27 -3
  77. package/deps/rocksdb/rocksdb/db/db_compaction_test.cc +850 -44
  78. package/deps/rocksdb/rocksdb/db/db_filesnapshot.cc +2 -29
  79. package/deps/rocksdb/rocksdb/db/db_flush_test.cc +275 -1
  80. package/deps/rocksdb/rocksdb/db/db_impl/compacted_db_impl.cc +52 -19
  81. package/deps/rocksdb/rocksdb/db/db_impl/compacted_db_impl.h +6 -5
  82. package/deps/rocksdb/rocksdb/db/db_impl/db_impl.cc +733 -320
  83. package/deps/rocksdb/rocksdb/db/db_impl/db_impl.h +155 -66
  84. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_compaction_flush.cc +516 -155
  85. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_debug.cc +8 -4
  86. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_experimental.cc +2 -1
  87. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_files.cc +17 -4
  88. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_open.cc +100 -35
  89. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_readonly.cc +95 -50
  90. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_readonly.h +13 -9
  91. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_secondary.cc +136 -79
  92. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_secondary.h +6 -95
  93. package/deps/rocksdb/rocksdb/db/db_impl/db_impl_write.cc +31 -22
  94. package/deps/rocksdb/rocksdb/db/db_info_dumper.cc +6 -0
  95. package/deps/rocksdb/rocksdb/db/db_iter.cc +85 -57
  96. package/deps/rocksdb/rocksdb/db/db_iter.h +11 -2
  97. package/deps/rocksdb/rocksdb/db/db_iter_test.cc +29 -0
  98. package/deps/rocksdb/rocksdb/db/db_iterator_test.cc +276 -21
  99. package/deps/rocksdb/rocksdb/db/db_log_iter_test.cc +35 -0
  100. package/deps/rocksdb/rocksdb/db/db_merge_operand_test.cc +4 -11
  101. package/deps/rocksdb/rocksdb/db/db_merge_operator_test.cc +193 -7
  102. package/deps/rocksdb/rocksdb/db/db_options_test.cc +294 -26
  103. package/deps/rocksdb/rocksdb/db/db_properties_test.cc +26 -36
  104. package/deps/rocksdb/rocksdb/db/db_range_del_test.cc +364 -0
  105. package/deps/rocksdb/rocksdb/db/db_rate_limiter_test.cc +13 -3
  106. package/deps/rocksdb/rocksdb/db/db_readonly_with_timestamp_test.cc +52 -0
  107. package/deps/rocksdb/rocksdb/db/db_secondary_test.cc +74 -1
  108. package/deps/rocksdb/rocksdb/db/db_sst_test.cc +22 -4
  109. package/deps/rocksdb/rocksdb/db/db_statistics_test.cc +1 -1
  110. package/deps/rocksdb/rocksdb/db/db_table_properties_test.cc +1 -0
  111. package/deps/rocksdb/rocksdb/db/db_tailing_iter_test.cc +282 -167
  112. package/deps/rocksdb/rocksdb/db/db_test.cc +180 -49
  113. package/deps/rocksdb/rocksdb/db/db_test2.cc +84 -12
  114. package/deps/rocksdb/rocksdb/db/db_test_util.cc +25 -12
  115. package/deps/rocksdb/rocksdb/db/db_test_util.h +45 -2
  116. package/deps/rocksdb/rocksdb/db/db_universal_compaction_test.cc +14 -1
  117. package/deps/rocksdb/rocksdb/db/db_wal_test.cc +245 -0
  118. package/deps/rocksdb/rocksdb/db/db_with_timestamp_basic_test.cc +480 -1
  119. package/deps/rocksdb/rocksdb/db/db_write_buffer_manager_test.cc +6 -6
  120. package/deps/rocksdb/rocksdb/db/db_write_test.cc +2 -2
  121. package/deps/rocksdb/rocksdb/db/dbformat.cc +36 -0
  122. package/deps/rocksdb/rocksdb/db/dbformat.h +169 -20
  123. package/deps/rocksdb/rocksdb/db/dbformat_test.cc +129 -0
  124. package/deps/rocksdb/rocksdb/db/deletefile_test.cc +2 -0
  125. package/deps/rocksdb/rocksdb/db/error_handler.cc +67 -34
  126. package/deps/rocksdb/rocksdb/db/error_handler.h +13 -9
  127. package/deps/rocksdb/rocksdb/db/error_handler_fs_test.cc +4 -4
  128. package/deps/rocksdb/rocksdb/db/event_helpers.cc +4 -0
  129. package/deps/rocksdb/rocksdb/db/experimental.cc +2 -1
  130. package/deps/rocksdb/rocksdb/db/external_sst_file_basic_test.cc +4 -4
  131. package/deps/rocksdb/rocksdb/db/external_sst_file_ingestion_job.cc +17 -8
  132. package/deps/rocksdb/rocksdb/db/external_sst_file_test.cc +144 -4
  133. package/deps/rocksdb/rocksdb/db/fault_injection_test.cc +1 -1
  134. package/deps/rocksdb/rocksdb/db/file_indexer.cc +2 -4
  135. package/deps/rocksdb/rocksdb/db/flush_job.cc +105 -17
  136. package/deps/rocksdb/rocksdb/db/flush_job.h +27 -4
  137. package/deps/rocksdb/rocksdb/db/flush_job_test.cc +90 -12
  138. package/deps/rocksdb/rocksdb/db/forward_iterator.cc +2 -3
  139. package/deps/rocksdb/rocksdb/db/import_column_family_job.cc +159 -91
  140. package/deps/rocksdb/rocksdb/db/import_column_family_job.h +19 -10
  141. package/deps/rocksdb/rocksdb/db/import_column_family_test.cc +143 -0
  142. package/deps/rocksdb/rocksdb/db/internal_stats.cc +13 -1
  143. package/deps/rocksdb/rocksdb/db/internal_stats.h +2 -0
  144. package/deps/rocksdb/rocksdb/db/listener_test.cc +2 -1
  145. package/deps/rocksdb/rocksdb/db/log_reader.h +3 -2
  146. package/deps/rocksdb/rocksdb/db/log_test.cc +17 -21
  147. package/deps/rocksdb/rocksdb/db/log_writer.cc +1 -1
  148. package/deps/rocksdb/rocksdb/db/log_writer.h +3 -2
  149. package/deps/rocksdb/rocksdb/db/manual_compaction_test.cc +6 -3
  150. package/deps/rocksdb/rocksdb/db/memtable.cc +70 -83
  151. package/deps/rocksdb/rocksdb/db/memtable.h +45 -1
  152. package/deps/rocksdb/rocksdb/db/memtable_list.cc +45 -11
  153. package/deps/rocksdb/rocksdb/db/memtable_list.h +43 -2
  154. package/deps/rocksdb/rocksdb/db/memtable_list_test.cc +91 -5
  155. package/deps/rocksdb/rocksdb/db/merge_helper.cc +330 -115
  156. package/deps/rocksdb/rocksdb/db/merge_helper.h +100 -12
  157. package/deps/rocksdb/rocksdb/db/merge_operator.cc +82 -0
  158. package/deps/rocksdb/rocksdb/db/merge_test.cc +267 -0
  159. package/deps/rocksdb/rocksdb/db/perf_context_test.cc +5 -2
  160. package/deps/rocksdb/rocksdb/db/periodic_task_scheduler.h +4 -4
  161. package/deps/rocksdb/rocksdb/db/plain_table_db_test.cc +3 -0
  162. package/deps/rocksdb/rocksdb/db/prefix_test.cc +1 -0
  163. package/deps/rocksdb/rocksdb/db/range_del_aggregator.h +4 -0
  164. package/deps/rocksdb/rocksdb/db/range_tombstone_fragmenter.h +4 -0
  165. package/deps/rocksdb/rocksdb/db/repair.cc +25 -7
  166. package/deps/rocksdb/rocksdb/db/repair_test.cc +143 -2
  167. package/deps/rocksdb/rocksdb/db/seqno_time_test.cc +459 -74
  168. package/deps/rocksdb/rocksdb/db/seqno_to_time_mapping.cc +105 -69
  169. package/deps/rocksdb/rocksdb/db/seqno_to_time_mapping.h +83 -46
  170. package/deps/rocksdb/rocksdb/db/table_cache.cc +76 -54
  171. package/deps/rocksdb/rocksdb/db/table_cache.h +18 -12
  172. package/deps/rocksdb/rocksdb/db/table_cache_sync_and_async.h +2 -2
  173. package/deps/rocksdb/rocksdb/db/version_builder.cc +0 -1
  174. package/deps/rocksdb/rocksdb/db/version_builder_test.cc +236 -204
  175. package/deps/rocksdb/rocksdb/db/version_edit.cc +66 -4
  176. package/deps/rocksdb/rocksdb/db/version_edit.h +58 -10
  177. package/deps/rocksdb/rocksdb/db/version_edit_handler.cc +80 -8
  178. package/deps/rocksdb/rocksdb/db/version_edit_handler.h +12 -0
  179. package/deps/rocksdb/rocksdb/db/version_edit_test.cc +86 -17
  180. package/deps/rocksdb/rocksdb/db/version_set.cc +207 -110
  181. package/deps/rocksdb/rocksdb/db/version_set.h +36 -15
  182. package/deps/rocksdb/rocksdb/db/version_set_sync_and_async.h +2 -5
  183. package/deps/rocksdb/rocksdb/db/version_set_test.cc +47 -26
  184. package/deps/rocksdb/rocksdb/db/wide/db_wide_basic_test.cc +525 -0
  185. package/deps/rocksdb/rocksdb/db/wide/wide_column_serialization.cc +6 -22
  186. package/deps/rocksdb/rocksdb/db/wide/wide_column_serialization.h +0 -20
  187. package/deps/rocksdb/rocksdb/db/wide/wide_column_serialization_test.cc +0 -29
  188. package/deps/rocksdb/rocksdb/db/wide/wide_columns_helper.cc +46 -0
  189. package/deps/rocksdb/rocksdb/db/wide/wide_columns_helper.h +40 -0
  190. package/deps/rocksdb/rocksdb/db/wide/wide_columns_helper_test.cc +39 -0
  191. package/deps/rocksdb/rocksdb/db/write_batch.cc +55 -20
  192. package/deps/rocksdb/rocksdb/db/write_batch_internal.h +3 -0
  193. package/deps/rocksdb/rocksdb/db/write_batch_test.cc +16 -0
  194. package/deps/rocksdb/rocksdb/db_stress_tool/CMakeLists.txt +1 -0
  195. package/deps/rocksdb/rocksdb/db_stress_tool/batched_ops_stress.cc +4 -4
  196. package/deps/rocksdb/rocksdb/db_stress_tool/cf_consistency_stress.cc +4 -7
  197. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_common.cc +88 -10
  198. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_common.h +37 -13
  199. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_driver.cc +110 -58
  200. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_env_wrapper.h +42 -0
  201. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_gflags.cc +68 -17
  202. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_listener.h +34 -0
  203. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_shared_state.h +8 -1
  204. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_test_base.cc +429 -237
  205. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_test_base.h +13 -6
  206. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_tool.cc +21 -14
  207. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_wide_merge_operator.cc +51 -0
  208. package/deps/rocksdb/rocksdb/db_stress_tool/db_stress_wide_merge_operator.h +27 -0
  209. package/deps/rocksdb/rocksdb/db_stress_tool/expected_state.cc +3 -6
  210. package/deps/rocksdb/rocksdb/db_stress_tool/expected_value.h +2 -0
  211. package/deps/rocksdb/rocksdb/db_stress_tool/multi_ops_txns_stress.cc +29 -38
  212. package/deps/rocksdb/rocksdb/db_stress_tool/no_batched_ops_stress.cc +302 -101
  213. package/deps/rocksdb/rocksdb/env/env.cc +6 -2
  214. package/deps/rocksdb/rocksdb/env/env_encryption.cc +11 -165
  215. package/deps/rocksdb/rocksdb/env/env_encryption_ctr.h +0 -17
  216. package/deps/rocksdb/rocksdb/env/env_posix.cc +6 -2
  217. package/deps/rocksdb/rocksdb/env/env_test.cc +86 -2
  218. package/deps/rocksdb/rocksdb/env/fs_posix.cc +6 -4
  219. package/deps/rocksdb/rocksdb/env/unique_id_gen.cc +79 -0
  220. package/deps/rocksdb/rocksdb/env/unique_id_gen.h +34 -0
  221. package/deps/rocksdb/rocksdb/file/delete_scheduler.cc +1 -0
  222. package/deps/rocksdb/rocksdb/file/delete_scheduler_test.cc +15 -4
  223. package/deps/rocksdb/rocksdb/file/file_prefetch_buffer.cc +100 -70
  224. package/deps/rocksdb/rocksdb/file/file_prefetch_buffer.h +64 -18
  225. package/deps/rocksdb/rocksdb/file/file_util.cc +10 -5
  226. package/deps/rocksdb/rocksdb/file/file_util.h +13 -1
  227. package/deps/rocksdb/rocksdb/file/prefetch_test.cc +1225 -97
  228. package/deps/rocksdb/rocksdb/file/random_access_file_reader.cc +72 -33
  229. package/deps/rocksdb/rocksdb/file/random_access_file_reader.h +3 -16
  230. package/deps/rocksdb/rocksdb/file/random_access_file_reader_test.cc +23 -12
  231. package/deps/rocksdb/rocksdb/file/sequence_file_reader.h +3 -0
  232. package/deps/rocksdb/rocksdb/include/rocksdb/advanced_cache.h +40 -14
  233. package/deps/rocksdb/rocksdb/include/rocksdb/advanced_options.h +163 -91
  234. package/deps/rocksdb/rocksdb/include/rocksdb/c.h +112 -2
  235. package/deps/rocksdb/rocksdb/include/rocksdb/cache.h +108 -16
  236. package/deps/rocksdb/rocksdb/include/rocksdb/compaction_filter.h +11 -0
  237. package/deps/rocksdb/rocksdb/include/rocksdb/compaction_job_stats.h +3 -0
  238. package/deps/rocksdb/rocksdb/include/rocksdb/comparator.h +42 -2
  239. package/deps/rocksdb/rocksdb/include/rocksdb/convenience.h +1 -1
  240. package/deps/rocksdb/rocksdb/include/rocksdb/db.h +92 -12
  241. package/deps/rocksdb/rocksdb/include/rocksdb/env.h +34 -4
  242. package/deps/rocksdb/rocksdb/include/rocksdb/env_encryption.h +9 -109
  243. package/deps/rocksdb/rocksdb/include/rocksdb/file_system.h +91 -13
  244. package/deps/rocksdb/rocksdb/include/rocksdb/filter_policy.h +8 -3
  245. package/deps/rocksdb/rocksdb/include/rocksdb/iterator.h +10 -4
  246. package/deps/rocksdb/rocksdb/include/rocksdb/listener.h +7 -0
  247. package/deps/rocksdb/rocksdb/include/rocksdb/memory_allocator.h +1 -1
  248. package/deps/rocksdb/rocksdb/include/rocksdb/merge_operator.h +55 -4
  249. package/deps/rocksdb/rocksdb/include/rocksdb/options.h +130 -22
  250. package/deps/rocksdb/rocksdb/include/rocksdb/port_defs.h +4 -0
  251. package/deps/rocksdb/rocksdb/include/rocksdb/rate_limiter.h +9 -0
  252. package/deps/rocksdb/rocksdb/include/rocksdb/secondary_cache.h +92 -9
  253. package/deps/rocksdb/rocksdb/include/rocksdb/sst_file_manager.h +2 -1
  254. package/deps/rocksdb/rocksdb/include/rocksdb/sst_file_writer.h +5 -1
  255. package/deps/rocksdb/rocksdb/include/rocksdb/statistics.h +37 -2
  256. package/deps/rocksdb/rocksdb/include/rocksdb/status.h +35 -0
  257. package/deps/rocksdb/rocksdb/include/rocksdb/system_clock.h +15 -0
  258. package/deps/rocksdb/rocksdb/include/rocksdb/table.h +7 -1
  259. package/deps/rocksdb/rocksdb/include/rocksdb/table_properties.h +20 -3
  260. package/deps/rocksdb/rocksdb/include/rocksdb/thread_status.h +7 -0
  261. package/deps/rocksdb/rocksdb/include/rocksdb/types.h +7 -0
  262. package/deps/rocksdb/rocksdb/include/rocksdb/utilities/ldb_cmd.h +6 -1
  263. package/deps/rocksdb/rocksdb/include/rocksdb/utilities/optimistic_transaction_db.h +33 -2
  264. package/deps/rocksdb/rocksdb/include/rocksdb/utilities/options_type.h +2 -1
  265. package/deps/rocksdb/rocksdb/include/rocksdb/utilities/stackable_db.h +14 -0
  266. package/deps/rocksdb/rocksdb/include/rocksdb/utilities/transaction.h +42 -2
  267. package/deps/rocksdb/rocksdb/include/rocksdb/utilities/write_batch_with_index.h +0 -3
  268. package/deps/rocksdb/rocksdb/include/rocksdb/version.h +2 -2
  269. package/deps/rocksdb/rocksdb/include/rocksdb/wide_columns.h +53 -2
  270. package/deps/rocksdb/rocksdb/include/rocksdb/write_batch.h +3 -2
  271. package/deps/rocksdb/rocksdb/memory/arena_test.cc +18 -11
  272. package/deps/rocksdb/rocksdb/memory/jemalloc_nodump_allocator.cc +4 -3
  273. package/deps/rocksdb/rocksdb/memory/jemalloc_nodump_allocator.h +1 -1
  274. package/deps/rocksdb/rocksdb/microbench/README.md +60 -0
  275. package/deps/rocksdb/rocksdb/microbench/db_basic_bench.cc +69 -34
  276. package/deps/rocksdb/rocksdb/monitoring/instrumented_mutex.h +1 -1
  277. package/deps/rocksdb/rocksdb/monitoring/statistics.cc +22 -1
  278. package/deps/rocksdb/rocksdb/monitoring/stats_history_test.cc +18 -7
  279. package/deps/rocksdb/rocksdb/monitoring/thread_status_util_debug.cc +14 -0
  280. package/deps/rocksdb/rocksdb/options/cf_options.cc +19 -0
  281. package/deps/rocksdb/rocksdb/options/cf_options.h +10 -2
  282. package/deps/rocksdb/rocksdb/options/customizable_test.cc +6 -1
  283. package/deps/rocksdb/rocksdb/options/db_options.cc +54 -2
  284. package/deps/rocksdb/rocksdb/options/db_options.h +4 -0
  285. package/deps/rocksdb/rocksdb/options/options.cc +15 -1
  286. package/deps/rocksdb/rocksdb/options/options_helper.cc +18 -0
  287. package/deps/rocksdb/rocksdb/options/options_settable_test.cc +14 -4
  288. package/deps/rocksdb/rocksdb/options/options_test.cc +14 -1
  289. package/deps/rocksdb/rocksdb/plugin/README.md +43 -0
  290. package/deps/rocksdb/rocksdb/port/README +10 -0
  291. package/deps/rocksdb/rocksdb/port/mmap.h +20 -0
  292. package/deps/rocksdb/rocksdb/port/port_example.h +1 -1
  293. package/deps/rocksdb/rocksdb/port/port_posix.cc +1 -1
  294. package/deps/rocksdb/rocksdb/port/port_posix.h +7 -4
  295. package/deps/rocksdb/rocksdb/port/stack_trace.cc +32 -12
  296. package/deps/rocksdb/rocksdb/port/win/env_win.h +1 -1
  297. package/deps/rocksdb/rocksdb/port/win/port_win.h +5 -2
  298. package/deps/rocksdb/rocksdb/src.mk +10 -1
  299. package/deps/rocksdb/rocksdb/table/block_based/binary_search_index_reader.cc +2 -1
  300. package/deps/rocksdb/rocksdb/table/block_based/block.cc +48 -22
  301. package/deps/rocksdb/rocksdb/table/block_based/block.h +60 -12
  302. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_builder.cc +116 -43
  303. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_factory.cc +9 -6
  304. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_iterator.cc +321 -49
  305. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_iterator.h +98 -4
  306. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_reader.cc +233 -98
  307. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_reader.h +58 -23
  308. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_reader_impl.h +12 -8
  309. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_reader_sync_and_async.h +52 -24
  310. package/deps/rocksdb/rocksdb/table/block_based/block_based_table_reader_test.cc +219 -51
  311. package/deps/rocksdb/rocksdb/table/block_based/block_builder.cc +41 -8
  312. package/deps/rocksdb/rocksdb/table/block_based/block_builder.h +25 -1
  313. package/deps/rocksdb/rocksdb/table/block_based/block_cache.cc +3 -1
  314. package/deps/rocksdb/rocksdb/table/block_based/block_cache.h +26 -7
  315. package/deps/rocksdb/rocksdb/table/block_based/block_prefetcher.cc +50 -18
  316. package/deps/rocksdb/rocksdb/table/block_based/block_prefetcher.h +20 -8
  317. package/deps/rocksdb/rocksdb/table/block_based/block_test.cc +232 -71
  318. package/deps/rocksdb/rocksdb/table/block_based/filter_block_reader_common.cc +6 -6
  319. package/deps/rocksdb/rocksdb/table/block_based/filter_policy.cc +44 -26
  320. package/deps/rocksdb/rocksdb/table/block_based/filter_policy_internal.h +2 -1
  321. package/deps/rocksdb/rocksdb/table/block_based/hash_index_reader.cc +1 -1
  322. package/deps/rocksdb/rocksdb/table/block_based/index_builder.cc +31 -16
  323. package/deps/rocksdb/rocksdb/table/block_based/index_builder.h +97 -58
  324. package/deps/rocksdb/rocksdb/table/block_based/index_reader_common.cc +2 -2
  325. package/deps/rocksdb/rocksdb/table/block_based/index_reader_common.h +6 -0
  326. package/deps/rocksdb/rocksdb/table/block_based/partitioned_filter_block.cc +36 -19
  327. package/deps/rocksdb/rocksdb/table/block_based/partitioned_filter_block.h +3 -1
  328. package/deps/rocksdb/rocksdb/table/block_based/partitioned_filter_block_test.cc +114 -70
  329. package/deps/rocksdb/rocksdb/table/block_based/partitioned_index_iterator.cc +4 -3
  330. package/deps/rocksdb/rocksdb/table/block_based/partitioned_index_reader.cc +11 -7
  331. package/deps/rocksdb/rocksdb/table/block_based/reader_common.cc +15 -3
  332. package/deps/rocksdb/rocksdb/table/block_based/reader_common.h +6 -3
  333. package/deps/rocksdb/rocksdb/table/block_based/uncompression_dict_reader.cc +1 -1
  334. package/deps/rocksdb/rocksdb/table/block_fetcher.cc +14 -13
  335. package/deps/rocksdb/rocksdb/table/block_fetcher.h +4 -0
  336. package/deps/rocksdb/rocksdb/table/block_fetcher_test.cc +9 -2
  337. package/deps/rocksdb/rocksdb/table/compaction_merging_iterator.cc +1 -0
  338. package/deps/rocksdb/rocksdb/table/cuckoo/cuckoo_table_builder.cc +6 -2
  339. package/deps/rocksdb/rocksdb/table/cuckoo/cuckoo_table_builder_test.cc +1 -2
  340. package/deps/rocksdb/rocksdb/table/cuckoo/cuckoo_table_reader.cc +2 -3
  341. package/deps/rocksdb/rocksdb/table/format.cc +175 -33
  342. package/deps/rocksdb/rocksdb/table/format.h +63 -10
  343. package/deps/rocksdb/rocksdb/table/get_context.cc +52 -89
  344. package/deps/rocksdb/rocksdb/table/get_context.h +12 -3
  345. package/deps/rocksdb/rocksdb/table/internal_iterator.h +11 -0
  346. package/deps/rocksdb/rocksdb/table/iterator_wrapper.h +29 -1
  347. package/deps/rocksdb/rocksdb/table/merging_iterator.cc +22 -2
  348. package/deps/rocksdb/rocksdb/table/meta_blocks.cc +12 -4
  349. package/deps/rocksdb/rocksdb/table/meta_blocks.h +1 -0
  350. package/deps/rocksdb/rocksdb/table/mock_table.cc +8 -3
  351. package/deps/rocksdb/rocksdb/table/plain/plain_table_builder.cc +10 -5
  352. package/deps/rocksdb/rocksdb/table/plain/plain_table_builder.h +10 -1
  353. package/deps/rocksdb/rocksdb/table/plain/plain_table_key_coding.cc +1 -2
  354. package/deps/rocksdb/rocksdb/table/plain/plain_table_reader.cc +3 -3
  355. package/deps/rocksdb/rocksdb/table/sst_file_dumper.cc +45 -9
  356. package/deps/rocksdb/rocksdb/table/sst_file_reader_test.cc +1 -0
  357. package/deps/rocksdb/rocksdb/table/sst_file_writer.cc +24 -1
  358. package/deps/rocksdb/rocksdb/table/table_builder.h +6 -2
  359. package/deps/rocksdb/rocksdb/table/table_properties.cc +6 -0
  360. package/deps/rocksdb/rocksdb/table/table_reader.h +6 -0
  361. package/deps/rocksdb/rocksdb/table/table_test.cc +52 -22
  362. package/deps/rocksdb/rocksdb/test_util/mock_time_env.h +31 -0
  363. package/deps/rocksdb/rocksdb/test_util/secondary_cache_test_util.cc +2 -1
  364. package/deps/rocksdb/rocksdb/test_util/secondary_cache_test_util.h +19 -7
  365. package/deps/rocksdb/rocksdb/test_util/sync_point.h +3 -1
  366. package/deps/rocksdb/rocksdb/test_util/testutil.cc +29 -0
  367. package/deps/rocksdb/rocksdb/test_util/testutil.h +19 -0
  368. package/deps/rocksdb/rocksdb/tools/block_cache_analyzer/block_cache_pysim.py +3 -3
  369. package/deps/rocksdb/rocksdb/tools/db_bench_tool.cc +87 -65
  370. package/deps/rocksdb/rocksdb/tools/ldb_cmd.cc +221 -33
  371. package/deps/rocksdb/rocksdb/tools/ldb_cmd_impl.h +36 -0
  372. package/deps/rocksdb/rocksdb/tools/ldb_tool.cc +1 -1
  373. package/deps/rocksdb/rocksdb/tools/reduce_levels_test.cc +1 -0
  374. package/deps/rocksdb/rocksdb/tools/sst_dump_test.cc +33 -11
  375. package/deps/rocksdb/rocksdb/tools/sst_dump_tool.cc +4 -0
  376. package/deps/rocksdb/rocksdb/unreleased_history/README.txt +73 -0
  377. package/deps/rocksdb/rocksdb/unreleased_history/add.sh +27 -0
  378. package/deps/rocksdb/rocksdb/unreleased_history/behavior_changes/.gitkeep +0 -0
  379. package/deps/rocksdb/rocksdb/unreleased_history/bug_fixes/.gitkeep +0 -0
  380. package/deps/rocksdb/rocksdb/unreleased_history/new_features/.gitkeep +0 -0
  381. package/deps/rocksdb/rocksdb/unreleased_history/performance_improvements/.gitkeep +0 -0
  382. package/deps/rocksdb/rocksdb/unreleased_history/public_api_changes/.gitkeep +0 -0
  383. package/deps/rocksdb/rocksdb/unreleased_history/release.sh +104 -0
  384. package/deps/rocksdb/rocksdb/util/async_file_reader.cc +5 -0
  385. package/deps/rocksdb/rocksdb/util/bloom_impl.h +3 -3
  386. package/deps/rocksdb/rocksdb/util/bloom_test.cc +32 -11
  387. package/deps/rocksdb/rocksdb/util/cast_util.h +24 -0
  388. package/deps/rocksdb/rocksdb/util/compaction_job_stats_impl.cc +2 -0
  389. package/deps/rocksdb/rocksdb/util/comparator.cc +55 -8
  390. package/deps/rocksdb/rocksdb/util/compression.cc +4 -4
  391. package/deps/rocksdb/rocksdb/util/compression.h +119 -35
  392. package/deps/rocksdb/rocksdb/util/core_local.h +2 -1
  393. package/deps/rocksdb/rocksdb/util/crc32c.cc +7 -1
  394. package/deps/rocksdb/rocksdb/util/distributed_mutex.h +1 -1
  395. package/deps/rocksdb/rocksdb/util/dynamic_bloom.h +4 -4
  396. package/deps/rocksdb/rocksdb/util/filelock_test.cc +3 -0
  397. package/deps/rocksdb/rocksdb/util/hash.h +7 -3
  398. package/deps/rocksdb/rocksdb/util/hash_test.cc +44 -0
  399. package/deps/rocksdb/rocksdb/util/math.h +58 -6
  400. package/deps/rocksdb/rocksdb/util/math128.h +29 -7
  401. package/deps/rocksdb/rocksdb/util/mutexlock.h +35 -27
  402. package/deps/rocksdb/rocksdb/util/overload.h +23 -0
  403. package/deps/rocksdb/rocksdb/util/rate_limiter.cc +53 -18
  404. package/deps/rocksdb/rocksdb/util/rate_limiter_impl.h +6 -1
  405. package/deps/rocksdb/rocksdb/util/rate_limiter_test.cc +90 -19
  406. package/deps/rocksdb/rocksdb/util/single_thread_executor.h +1 -0
  407. package/deps/rocksdb/rocksdb/util/slice_test.cc +30 -0
  408. package/deps/rocksdb/rocksdb/util/status.cc +1 -0
  409. package/deps/rocksdb/rocksdb/util/stop_watch.h +1 -1
  410. package/deps/rocksdb/rocksdb/util/string_util.cc +39 -0
  411. package/deps/rocksdb/rocksdb/util/string_util.h +10 -0
  412. package/deps/rocksdb/rocksdb/util/thread_operation.h +10 -1
  413. package/deps/rocksdb/rocksdb/util/udt_util.cc +385 -0
  414. package/deps/rocksdb/rocksdb/util/udt_util.h +192 -1
  415. package/deps/rocksdb/rocksdb/util/udt_util_test.cc +461 -0
  416. package/deps/rocksdb/rocksdb/util/write_batch_util.cc +25 -0
  417. package/deps/rocksdb/rocksdb/util/write_batch_util.h +80 -0
  418. package/deps/rocksdb/rocksdb/util/xxhash.h +0 -3
  419. package/deps/rocksdb/rocksdb/util/xxph3.h +0 -4
  420. package/deps/rocksdb/rocksdb/utilities/backup/backup_engine_test.cc +4 -4
  421. package/deps/rocksdb/rocksdb/utilities/blob_db/blob_db_impl.cc +71 -26
  422. package/deps/rocksdb/rocksdb/utilities/blob_db/blob_db_impl.h +7 -6
  423. package/deps/rocksdb/rocksdb/utilities/blob_db/blob_db_listener.h +1 -1
  424. package/deps/rocksdb/rocksdb/utilities/blob_db/blob_dump_tool.cc +2 -3
  425. package/deps/rocksdb/rocksdb/utilities/blob_db/blob_file.cc +6 -11
  426. package/deps/rocksdb/rocksdb/utilities/cache_dump_load_impl.h +1 -2
  427. package/deps/rocksdb/rocksdb/utilities/checkpoint/checkpoint_test.cc +4 -5
  428. package/deps/rocksdb/rocksdb/utilities/fault_injection_env.h +1 -0
  429. package/deps/rocksdb/rocksdb/utilities/fault_injection_fs.cc +20 -16
  430. package/deps/rocksdb/rocksdb/utilities/fault_injection_fs.h +11 -7
  431. package/deps/rocksdb/rocksdb/utilities/fault_injection_secondary_cache.cc +2 -2
  432. package/deps/rocksdb/rocksdb/utilities/fault_injection_secondary_cache.h +7 -1
  433. package/deps/rocksdb/rocksdb/utilities/merge_operators/string_append/stringappend_test.cc +3 -0
  434. package/deps/rocksdb/rocksdb/utilities/option_change_migration/option_change_migration_test.cc +12 -3
  435. package/deps/rocksdb/rocksdb/utilities/persistent_cache/block_cache_tier_file.cc +1 -2
  436. package/deps/rocksdb/rocksdb/utilities/simulator_cache/sim_cache.cc +7 -4
  437. package/deps/rocksdb/rocksdb/utilities/trace/file_trace_reader_writer.cc +2 -3
  438. package/deps/rocksdb/rocksdb/utilities/transactions/lock/point/point_lock_manager_test.cc +2 -2
  439. package/deps/rocksdb/rocksdb/utilities/transactions/lock/point/point_lock_manager_test.h +1 -1
  440. package/deps/rocksdb/rocksdb/utilities/transactions/lock/range/range_tree/lib/README +13 -0
  441. package/deps/rocksdb/rocksdb/utilities/transactions/optimistic_transaction.cc +23 -8
  442. package/deps/rocksdb/rocksdb/utilities/transactions/optimistic_transaction_db_impl.cc +9 -6
  443. package/deps/rocksdb/rocksdb/utilities/transactions/optimistic_transaction_db_impl.h +37 -12
  444. package/deps/rocksdb/rocksdb/utilities/transactions/optimistic_transaction_test.cc +272 -33
  445. package/deps/rocksdb/rocksdb/utilities/transactions/pessimistic_transaction.cc +15 -9
  446. package/deps/rocksdb/rocksdb/utilities/transactions/pessimistic_transaction.h +4 -1
  447. package/deps/rocksdb/rocksdb/utilities/transactions/transaction_base.cc +76 -20
  448. package/deps/rocksdb/rocksdb/utilities/transactions/transaction_base.h +18 -9
  449. package/deps/rocksdb/rocksdb/utilities/transactions/transaction_test.cc +195 -23
  450. package/deps/rocksdb/rocksdb/utilities/transactions/transaction_test.h +19 -12
  451. package/deps/rocksdb/rocksdb/utilities/transactions/write_committed_transaction_ts_test.cc +88 -1
  452. package/deps/rocksdb/rocksdb/utilities/transactions/write_prepared_transaction_test.cc +1 -1
  453. package/deps/rocksdb/rocksdb/utilities/transactions/write_prepared_txn.cc +43 -17
  454. package/deps/rocksdb/rocksdb/utilities/transactions/write_prepared_txn.h +6 -3
  455. package/deps/rocksdb/rocksdb/utilities/transactions/write_prepared_txn_db.cc +73 -24
  456. package/deps/rocksdb/rocksdb/utilities/transactions/write_prepared_txn_db.h +19 -4
  457. package/deps/rocksdb/rocksdb/utilities/transactions/write_unprepared_transaction_test.cc +60 -107
  458. package/deps/rocksdb/rocksdb/utilities/transactions/write_unprepared_txn.cc +41 -12
  459. package/deps/rocksdb/rocksdb/utilities/transactions/write_unprepared_txn.h +6 -3
  460. package/deps/rocksdb/rocksdb/utilities/transactions/write_unprepared_txn_db.cc +15 -8
  461. package/deps/rocksdb/rocksdb/utilities/transactions/write_unprepared_txn_db.h +1 -1
  462. package/deps/rocksdb/rocksdb/utilities/ttl/db_ttl_impl.cc +10 -5
  463. package/deps/rocksdb/rocksdb/utilities/ttl/db_ttl_impl.h +1 -1
  464. package/deps/rocksdb/rocksdb/utilities/ttl/ttl_test.cc +1 -1
  465. package/deps/rocksdb/rocksdb/utilities/write_batch_with_index/write_batch_with_index.cc +59 -28
  466. package/deps/rocksdb/rocksdb/utilities/write_batch_with_index/write_batch_with_index_internal.cc +127 -120
  467. package/deps/rocksdb/rocksdb/utilities/write_batch_with_index/write_batch_with_index_internal.h +129 -59
  468. package/deps/rocksdb/rocksdb/utilities/write_batch_with_index/write_batch_with_index_test.cc +111 -14
  469. package/deps/rocksdb/rocksdb.gyp +6 -2
  470. package/index.js +0 -8
  471. package/package.json +1 -1
  472. package/prebuilds/darwin-arm64/node.napi.node +0 -0
  473. package/prebuilds/linux-x64/node.napi.node +0 -0
  474. package/deps/rocksdb/rocksdb/cmake/modules/CxxFlags.cmake +0 -7
  475. package/deps/rocksdb/rocksdb/cmake/modules/FindJeMalloc.cmake +0 -29
  476. package/deps/rocksdb/rocksdb/cmake/modules/FindNUMA.cmake +0 -29
  477. package/deps/rocksdb/rocksdb/cmake/modules/FindSnappy.cmake +0 -29
  478. package/deps/rocksdb/rocksdb/cmake/modules/FindTBB.cmake +0 -33
  479. package/deps/rocksdb/rocksdb/cmake/modules/Findgflags.cmake +0 -29
  480. package/deps/rocksdb/rocksdb/cmake/modules/Findlz4.cmake +0 -29
  481. package/deps/rocksdb/rocksdb/cmake/modules/Finduring.cmake +0 -26
  482. package/deps/rocksdb/rocksdb/cmake/modules/Findzstd.cmake +0 -29
  483. package/deps/rocksdb/rocksdb/cmake/modules/ReadVersion.cmake +0 -10
@@ -9,8 +9,18 @@
9
9
 
10
10
  #include "cache/clock_cache.h"
11
11
 
12
+ #include <algorithm>
13
+ #include <atomic>
14
+ #include <bitset>
15
+ #include <cassert>
16
+ #include <cinttypes>
17
+ #include <cstddef>
18
+ #include <exception>
12
19
  #include <functional>
13
20
  #include <numeric>
21
+ #include <string>
22
+ #include <thread>
23
+ #include <type_traits>
14
24
 
15
25
  #include "cache/cache_key.h"
16
26
  #include "cache/secondary_cache_adapter.h"
@@ -72,23 +82,52 @@ inline void FreeDataMarkEmpty(ClockHandle& h, MemoryAllocator* allocator) {
72
82
  MarkEmpty(h);
73
83
  }
74
84
 
75
- inline bool ClockUpdate(ClockHandle& h) {
76
- uint64_t meta = h.meta.load(std::memory_order_relaxed);
85
+ // Called to undo the effect of referencing an entry for internal purposes,
86
+ // so it should not be marked as having been used.
87
+ inline void Unref(const ClockHandle& h, uint64_t count = 1) {
88
+ // Pretend we never took the reference
89
+ // WART: there's a tiny chance we release last ref to invisible
90
+ // entry here. If that happens, we let eviction take care of it.
91
+ uint64_t old_meta = h.meta.fetch_sub(ClockHandle::kAcquireIncrement * count,
92
+ std::memory_order_release);
93
+ assert(GetRefcount(old_meta) != 0);
94
+ (void)old_meta;
95
+ }
96
+
97
+ inline bool ClockUpdate(ClockHandle& h, bool* purgeable = nullptr) {
98
+ uint64_t meta;
99
+ if (purgeable) {
100
+ assert(*purgeable == false);
101
+ // In AutoHCC, our eviction process follows the chain structure, so we
102
+ // should ensure that we see the latest state of each entry, at least for
103
+ // assertion checking.
104
+ meta = h.meta.load(std::memory_order_acquire);
105
+ } else {
106
+ // In FixedHCC, our eviction process is a simple iteration without regard
107
+ // to probing order, displacements, etc., so it doesn't matter if we see
108
+ // somewhat stale data.
109
+ meta = h.meta.load(std::memory_order_relaxed);
110
+ }
77
111
 
112
+ if (((meta >> ClockHandle::kStateShift) & ClockHandle::kStateShareableBit) ==
113
+ 0) {
114
+ // Only clock update Shareable entries
115
+ if (purgeable) {
116
+ *purgeable = true;
117
+ // AutoHCC only: make sure we only attempt to update non-empty slots
118
+ assert((meta >> ClockHandle::kStateShift) &
119
+ ClockHandle::kStateOccupiedBit);
120
+ }
121
+ return false;
122
+ }
78
123
  uint64_t acquire_count =
79
124
  (meta >> ClockHandle::kAcquireCounterShift) & ClockHandle::kCounterMask;
80
125
  uint64_t release_count =
81
126
  (meta >> ClockHandle::kReleaseCounterShift) & ClockHandle::kCounterMask;
82
- // fprintf(stderr, "ClockUpdate @ %p: %lu %lu %u\n", &h, acquire_count,
83
- // release_count, (unsigned)(meta >> ClockHandle::kStateShift));
84
127
  if (acquire_count != release_count) {
85
128
  // Only clock update entries with no outstanding refs
86
129
  return false;
87
130
  }
88
- if (!((meta >> ClockHandle::kStateShift) & ClockHandle::kStateShareableBit)) {
89
- // Only clock update Shareable entries
90
- return false;
91
- }
92
131
  if ((meta >> ClockHandle::kStateShift == ClockHandle::kStateVisible) &&
93
132
  acquire_count > 0) {
94
133
  // Decrement clock
@@ -98,6 +137,7 @@ inline bool ClockUpdate(ClockHandle& h) {
98
137
  // not aggressively
99
138
  uint64_t new_meta =
100
139
  (uint64_t{ClockHandle::kStateVisible} << ClockHandle::kStateShift) |
140
+ (meta & ClockHandle::kHitBitMask) |
101
141
  (new_count << ClockHandle::kReleaseCounterShift) |
102
142
  (new_count << ClockHandle::kAcquireCounterShift);
103
143
  h.meta.compare_exchange_strong(meta, new_meta, std::memory_order_relaxed);
@@ -105,10 +145,11 @@ inline bool ClockUpdate(ClockHandle& h) {
105
145
  }
106
146
  // Otherwise, remove entry (either unreferenced invisible or
107
147
  // unreferenced and expired visible).
108
- if (h.meta.compare_exchange_strong(
109
- meta,
110
- uint64_t{ClockHandle::kStateConstruction} << ClockHandle::kStateShift,
111
- std::memory_order_acquire)) {
148
+ if (h.meta.compare_exchange_strong(meta,
149
+ (uint64_t{ClockHandle::kStateConstruction}
150
+ << ClockHandle::kStateShift) |
151
+ (meta & ClockHandle::kHitBitMask),
152
+ std::memory_order_acquire)) {
112
153
  // Took ownership.
113
154
  return true;
114
155
  } else {
@@ -118,74 +159,6 @@ inline bool ClockUpdate(ClockHandle& h) {
118
159
  }
119
160
  }
120
161
 
121
- } // namespace
122
-
123
- void ClockHandleBasicData::FreeData(MemoryAllocator* allocator) const {
124
- if (helper->del_cb) {
125
- helper->del_cb(value, allocator);
126
- }
127
- }
128
-
129
- HyperClockTable::HyperClockTable(
130
- size_t capacity, bool /*strict_capacity_limit*/,
131
- CacheMetadataChargePolicy metadata_charge_policy,
132
- MemoryAllocator* allocator,
133
- const Cache::EvictionCallback* eviction_callback, const uint32_t* hash_seed,
134
- const Opts& opts)
135
- : length_bits_(CalcHashBits(capacity, opts.estimated_value_size,
136
- metadata_charge_policy)),
137
- length_bits_mask_((size_t{1} << length_bits_) - 1),
138
- occupancy_limit_(static_cast<size_t>((uint64_t{1} << length_bits_) *
139
- kStrictLoadFactor)),
140
- array_(new HandleImpl[size_t{1} << length_bits_]),
141
- allocator_(allocator),
142
- eviction_callback_(*eviction_callback),
143
- hash_seed_(*hash_seed) {
144
- if (metadata_charge_policy ==
145
- CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
146
- usage_ += size_t{GetTableSize()} * sizeof(HandleImpl);
147
- }
148
-
149
- static_assert(sizeof(HandleImpl) == 64U,
150
- "Expecting size / alignment with common cache line size");
151
- }
152
-
153
- HyperClockTable::~HyperClockTable() {
154
- // Assumes there are no references or active operations on any slot/element
155
- // in the table.
156
- for (size_t i = 0; i < GetTableSize(); i++) {
157
- HandleImpl& h = array_[i];
158
- switch (h.meta >> ClockHandle::kStateShift) {
159
- case ClockHandle::kStateEmpty:
160
- // noop
161
- break;
162
- case ClockHandle::kStateInvisible: // rare but possible
163
- case ClockHandle::kStateVisible:
164
- assert(GetRefcount(h.meta) == 0);
165
- h.FreeData(allocator_);
166
- #ifndef NDEBUG
167
- Rollback(h.hashed_key, &h);
168
- ReclaimEntryUsage(h.GetTotalCharge());
169
- #endif
170
- break;
171
- // otherwise
172
- default:
173
- assert(false);
174
- break;
175
- }
176
- }
177
-
178
- #ifndef NDEBUG
179
- for (size_t i = 0; i < GetTableSize(); i++) {
180
- assert(array_[i].displacements.load() == 0);
181
- }
182
- #endif
183
-
184
- assert(usage_.load() == 0 ||
185
- usage_.load() == size_t{GetTableSize()} * sizeof(HandleImpl));
186
- assert(occupancy_ == 0);
187
- }
188
-
189
162
  // If an entry doesn't receive clock updates but is repeatedly referenced &
190
163
  // released, the acquire and release counters could overflow without some
191
164
  // intervention. This is that intervention, which should be inexpensive
@@ -259,8 +232,202 @@ inline void CorrectNearOverflow(uint64_t old_meta,
259
232
  }
260
233
  }
261
234
 
262
- inline Status HyperClockTable::ChargeUsageMaybeEvictStrict(
263
- size_t total_charge, size_t capacity, bool need_evict_for_occupancy) {
235
+ inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
236
+ uint64_t initial_countdown, bool* already_matches) {
237
+ assert(*already_matches == false);
238
+ // Optimistically transition the slot from "empty" to
239
+ // "under construction" (no effect on other states)
240
+ uint64_t old_meta = h.meta.fetch_or(
241
+ uint64_t{ClockHandle::kStateOccupiedBit} << ClockHandle::kStateShift,
242
+ std::memory_order_acq_rel);
243
+ uint64_t old_state = old_meta >> ClockHandle::kStateShift;
244
+
245
+ if (old_state == ClockHandle::kStateEmpty) {
246
+ // We've started inserting into an available slot, and taken
247
+ // ownership.
248
+ return true;
249
+ } else if (old_state != ClockHandle::kStateVisible) {
250
+ // Slot not usable / touchable now
251
+ return false;
252
+ }
253
+ // Existing, visible entry, which might be a match.
254
+ // But first, we need to acquire a ref to read it. In fact, number of
255
+ // refs for initial countdown, so that we boost the clock state if
256
+ // this is a match.
257
+ old_meta =
258
+ h.meta.fetch_add(ClockHandle::kAcquireIncrement * initial_countdown,
259
+ std::memory_order_acq_rel);
260
+ // Like Lookup
261
+ if ((old_meta >> ClockHandle::kStateShift) == ClockHandle::kStateVisible) {
262
+ // Acquired a read reference
263
+ if (h.hashed_key == proto.hashed_key) {
264
+ // Match. Release in a way that boosts the clock state
265
+ old_meta =
266
+ h.meta.fetch_add(ClockHandle::kReleaseIncrement * initial_countdown,
267
+ std::memory_order_acq_rel);
268
+ // Correct for possible (but rare) overflow
269
+ CorrectNearOverflow(old_meta, h.meta);
270
+ // Insert detached instead (only if return handle needed)
271
+ *already_matches = true;
272
+ return false;
273
+ } else {
274
+ // Mismatch.
275
+ Unref(h, initial_countdown);
276
+ }
277
+ } else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
278
+ ClockHandle::kStateInvisible)) {
279
+ // Pretend we never took the reference
280
+ Unref(h, initial_countdown);
281
+ } else {
282
+ // For other states, incrementing the acquire counter has no effect
283
+ // so we don't need to undo it.
284
+ // Slot not usable / touchable now.
285
+ }
286
+ return false;
287
+ }
288
+
289
+ inline void FinishSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
290
+ uint64_t initial_countdown, bool keep_ref) {
291
+ // Save data fields
292
+ ClockHandleBasicData* h_alias = &h;
293
+ *h_alias = proto;
294
+
295
+ // Transition from "under construction" state to "visible" state
296
+ uint64_t new_meta = uint64_t{ClockHandle::kStateVisible}
297
+ << ClockHandle::kStateShift;
298
+
299
+ // Maybe with an outstanding reference
300
+ new_meta |= initial_countdown << ClockHandle::kAcquireCounterShift;
301
+ new_meta |= (initial_countdown - keep_ref)
302
+ << ClockHandle::kReleaseCounterShift;
303
+
304
+ #ifndef NDEBUG
305
+ // Save the state transition, with assertion
306
+ uint64_t old_meta = h.meta.exchange(new_meta, std::memory_order_release);
307
+ assert(old_meta >> ClockHandle::kStateShift ==
308
+ ClockHandle::kStateConstruction);
309
+ #else
310
+ // Save the state transition
311
+ h.meta.store(new_meta, std::memory_order_release);
312
+ #endif
313
+ }
314
+
315
+ bool TryInsert(const ClockHandleBasicData& proto, ClockHandle& h,
316
+ uint64_t initial_countdown, bool keep_ref,
317
+ bool* already_matches) {
318
+ bool b = BeginSlotInsert(proto, h, initial_countdown, already_matches);
319
+ if (b) {
320
+ FinishSlotInsert(proto, h, initial_countdown, keep_ref);
321
+ }
322
+ return b;
323
+ }
324
+
325
+ // Func must be const HandleImpl& -> void callable
326
+ template <class HandleImpl, class Func>
327
+ void ConstApplyToEntriesRange(const Func& func, const HandleImpl* begin,
328
+ const HandleImpl* end,
329
+ bool apply_if_will_be_deleted) {
330
+ uint64_t check_state_mask = ClockHandle::kStateShareableBit;
331
+ if (!apply_if_will_be_deleted) {
332
+ check_state_mask |= ClockHandle::kStateVisibleBit;
333
+ }
334
+
335
+ for (const HandleImpl* h = begin; h < end; ++h) {
336
+ // Note: to avoid using compare_exchange, we have to be extra careful.
337
+ uint64_t old_meta = h->meta.load(std::memory_order_relaxed);
338
+ // Check if it's an entry visible to lookups
339
+ if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
340
+ // Increment acquire counter. Note: it's possible that the entry has
341
+ // completely changed since we loaded old_meta, but incrementing acquire
342
+ // count is always safe. (Similar to optimistic Lookup here.)
343
+ old_meta = h->meta.fetch_add(ClockHandle::kAcquireIncrement,
344
+ std::memory_order_acquire);
345
+ // Check whether we actually acquired a reference.
346
+ if ((old_meta >> ClockHandle::kStateShift) &
347
+ ClockHandle::kStateShareableBit) {
348
+ // Apply func if appropriate
349
+ if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
350
+ func(*h);
351
+ }
352
+ // Pretend we never took the reference
353
+ Unref(*h);
354
+ // No net change, so don't need to check for overflow
355
+ } else {
356
+ // For other states, incrementing the acquire counter has no effect
357
+ // so we don't need to undo it. Furthermore, we cannot safely undo
358
+ // it because we did not acquire a read reference to lock the
359
+ // entry in a Shareable state.
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ } // namespace
366
+
367
+ void ClockHandleBasicData::FreeData(MemoryAllocator* allocator) const {
368
+ if (helper->del_cb) {
369
+ helper->del_cb(value, allocator);
370
+ }
371
+ }
372
+
373
+ template <class HandleImpl>
374
+ HandleImpl* BaseClockTable::StandaloneInsert(
375
+ const ClockHandleBasicData& proto) {
376
+ // Heap allocated separate from table
377
+ HandleImpl* h = new HandleImpl();
378
+ ClockHandleBasicData* h_alias = h;
379
+ *h_alias = proto;
380
+ h->SetStandalone();
381
+ // Single reference (standalone entries only created if returning a refed
382
+ // Handle back to user)
383
+ uint64_t meta = uint64_t{ClockHandle::kStateInvisible}
384
+ << ClockHandle::kStateShift;
385
+ meta |= uint64_t{1} << ClockHandle::kAcquireCounterShift;
386
+ h->meta.store(meta, std::memory_order_release);
387
+ // Keep track of how much of usage is standalone
388
+ standalone_usage_.fetch_add(proto.GetTotalCharge(),
389
+ std::memory_order_relaxed);
390
+ return h;
391
+ }
392
+
393
+ template <class Table>
394
+ typename Table::HandleImpl* BaseClockTable::CreateStandalone(
395
+ ClockHandleBasicData& proto, size_t capacity, bool strict_capacity_limit,
396
+ bool allow_uncharged) {
397
+ Table& derived = static_cast<Table&>(*this);
398
+ typename Table::InsertState state;
399
+ derived.StartInsert(state);
400
+
401
+ const size_t total_charge = proto.GetTotalCharge();
402
+ if (strict_capacity_limit) {
403
+ Status s = ChargeUsageMaybeEvictStrict<Table>(
404
+ total_charge, capacity,
405
+ /*need_evict_for_occupancy=*/false, state);
406
+ if (!s.ok()) {
407
+ if (allow_uncharged) {
408
+ proto.total_charge = 0;
409
+ } else {
410
+ return nullptr;
411
+ }
412
+ }
413
+ } else {
414
+ // Case strict_capacity_limit == false
415
+ bool success = ChargeUsageMaybeEvictNonStrict<Table>(
416
+ total_charge, capacity,
417
+ /*need_evict_for_occupancy=*/false, state);
418
+ if (!success) {
419
+ // Force the issue
420
+ usage_.fetch_add(total_charge, std::memory_order_relaxed);
421
+ }
422
+ }
423
+
424
+ return StandaloneInsert<typename Table::HandleImpl>(proto);
425
+ }
426
+
427
+ template <class Table>
428
+ Status BaseClockTable::ChargeUsageMaybeEvictStrict(
429
+ size_t total_charge, size_t capacity, bool need_evict_for_occupancy,
430
+ typename Table::InsertState& state) {
264
431
  if (total_charge > capacity) {
265
432
  return Status::MemoryLimit(
266
433
  "Cache entry too large for a single cache shard: " +
@@ -269,14 +436,14 @@ inline Status HyperClockTable::ChargeUsageMaybeEvictStrict(
269
436
  // Grab any available capacity, and free up any more required.
270
437
  size_t old_usage = usage_.load(std::memory_order_relaxed);
271
438
  size_t new_usage;
272
- if (LIKELY(old_usage != capacity)) {
273
- do {
274
- new_usage = std::min(capacity, old_usage + total_charge);
275
- } while (!usage_.compare_exchange_weak(old_usage, new_usage,
276
- std::memory_order_relaxed));
277
- } else {
278
- new_usage = old_usage;
279
- }
439
+ do {
440
+ new_usage = std::min(capacity, old_usage + total_charge);
441
+ if (new_usage == old_usage) {
442
+ // No change needed
443
+ break;
444
+ }
445
+ } while (!usage_.compare_exchange_weak(old_usage, new_usage,
446
+ std::memory_order_relaxed));
280
447
  // How much do we need to evict then?
281
448
  size_t need_evict_charge = old_usage + total_charge - new_usage;
282
449
  size_t request_evict_charge = need_evict_charge;
@@ -285,21 +452,20 @@ inline Status HyperClockTable::ChargeUsageMaybeEvictStrict(
285
452
  request_evict_charge = 1;
286
453
  }
287
454
  if (request_evict_charge > 0) {
288
- size_t evicted_charge = 0;
289
- size_t evicted_count = 0;
290
- Evict(request_evict_charge, &evicted_charge, &evicted_count);
291
- occupancy_.fetch_sub(evicted_count, std::memory_order_release);
292
- if (LIKELY(evicted_charge > need_evict_charge)) {
293
- assert(evicted_count > 0);
455
+ EvictionData data;
456
+ static_cast<Table*>(this)->Evict(request_evict_charge, state, &data);
457
+ occupancy_.fetch_sub(data.freed_count, std::memory_order_release);
458
+ if (LIKELY(data.freed_charge > need_evict_charge)) {
459
+ assert(data.freed_count > 0);
294
460
  // Evicted more than enough
295
- usage_.fetch_sub(evicted_charge - need_evict_charge,
461
+ usage_.fetch_sub(data.freed_charge - need_evict_charge,
296
462
  std::memory_order_relaxed);
297
- } else if (evicted_charge < need_evict_charge ||
298
- (UNLIKELY(need_evict_for_occupancy) && evicted_count == 0)) {
463
+ } else if (data.freed_charge < need_evict_charge ||
464
+ (UNLIKELY(need_evict_for_occupancy) && data.freed_count == 0)) {
299
465
  // Roll back to old usage minus evicted
300
- usage_.fetch_sub(evicted_charge + (new_usage - old_usage),
466
+ usage_.fetch_sub(data.freed_charge + (new_usage - old_usage),
301
467
  std::memory_order_relaxed);
302
- if (evicted_charge < need_evict_charge) {
468
+ if (data.freed_charge < need_evict_charge) {
303
469
  return Status::MemoryLimit(
304
470
  "Insert failed because unable to evict entries to stay within "
305
471
  "capacity limit.");
@@ -311,13 +477,15 @@ inline Status HyperClockTable::ChargeUsageMaybeEvictStrict(
311
477
  }
312
478
  // If we needed to evict something and we are proceeding, we must have
313
479
  // evicted something.
314
- assert(evicted_count > 0);
480
+ assert(data.freed_count > 0);
315
481
  }
316
482
  return Status::OK();
317
483
  }
318
484
 
319
- inline bool HyperClockTable::ChargeUsageMaybeEvictNonStrict(
320
- size_t total_charge, size_t capacity, bool need_evict_for_occupancy) {
485
+ template <class Table>
486
+ inline bool BaseClockTable::ChargeUsageMaybeEvictNonStrict(
487
+ size_t total_charge, size_t capacity, bool need_evict_for_occupancy,
488
+ typename Table::InsertState& state) {
321
489
  // For simplicity, we consider that either the cache can accept the insert
322
490
  // with no evictions, or we must evict enough to make (at least) enough
323
491
  // space. It could lead to unnecessary failures or excessive evictions in
@@ -351,76 +519,85 @@ inline bool HyperClockTable::ChargeUsageMaybeEvictNonStrict(
351
519
  // deal with occupancy
352
520
  need_evict_charge = 1;
353
521
  }
354
- size_t evicted_charge = 0;
355
- size_t evicted_count = 0;
522
+ EvictionData data;
356
523
  if (need_evict_charge > 0) {
357
- Evict(need_evict_charge, &evicted_charge, &evicted_count);
524
+ static_cast<Table*>(this)->Evict(need_evict_charge, state, &data);
358
525
  // Deal with potential occupancy deficit
359
- if (UNLIKELY(need_evict_for_occupancy) && evicted_count == 0) {
360
- assert(evicted_charge == 0);
526
+ if (UNLIKELY(need_evict_for_occupancy) && data.freed_count == 0) {
527
+ assert(data.freed_charge == 0);
361
528
  // Can't meet occupancy requirement
362
529
  return false;
363
530
  } else {
364
531
  // Update occupancy for evictions
365
- occupancy_.fetch_sub(evicted_count, std::memory_order_release);
532
+ occupancy_.fetch_sub(data.freed_count, std::memory_order_release);
366
533
  }
367
534
  }
368
535
  // Track new usage even if we weren't able to evict enough
369
- usage_.fetch_add(total_charge - evicted_charge, std::memory_order_relaxed);
536
+ usage_.fetch_add(total_charge - data.freed_charge, std::memory_order_relaxed);
370
537
  // No underflow
371
538
  assert(usage_.load(std::memory_order_relaxed) < SIZE_MAX / 2);
372
539
  // Success
373
540
  return true;
374
541
  }
375
542
 
376
- inline HyperClockTable::HandleImpl* HyperClockTable::StandaloneInsert(
377
- const ClockHandleBasicData& proto) {
378
- // Heap allocated separate from table
379
- HandleImpl* h = new HandleImpl();
380
- ClockHandleBasicData* h_alias = h;
381
- *h_alias = proto;
382
- h->SetStandalone();
383
- // Single reference (standalone entries only created if returning a refed
384
- // Handle back to user)
385
- uint64_t meta = uint64_t{ClockHandle::kStateInvisible}
386
- << ClockHandle::kStateShift;
387
- meta |= uint64_t{1} << ClockHandle::kAcquireCounterShift;
388
- h->meta.store(meta, std::memory_order_release);
389
- // Keep track of how much of usage is standalone
390
- standalone_usage_.fetch_add(proto.GetTotalCharge(),
391
- std::memory_order_relaxed);
392
- return h;
543
+ void BaseClockTable::TrackAndReleaseEvictedEntry(
544
+ ClockHandle* h, BaseClockTable::EvictionData* data) {
545
+ data->freed_charge += h->GetTotalCharge();
546
+ data->freed_count += 1;
547
+
548
+ bool took_value_ownership = false;
549
+ if (eviction_callback_) {
550
+ // For key reconstructed from hash
551
+ UniqueId64x2 unhashed;
552
+ took_value_ownership = eviction_callback_(
553
+ ClockCacheShard<FixedHyperClockTable>::ReverseHash(
554
+ h->GetHash(), &unhashed, hash_seed_),
555
+ reinterpret_cast<Cache::Handle*>(h),
556
+ h->meta.load(std::memory_order_relaxed) & ClockHandle::kHitBitMask);
557
+ }
558
+ if (!took_value_ownership) {
559
+ h->FreeData(allocator_);
560
+ }
561
+ MarkEmpty(*h);
393
562
  }
394
563
 
395
- Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
396
- HandleImpl** handle, Cache::Priority priority,
397
- size_t capacity, bool strict_capacity_limit) {
564
+ template <class Table>
565
+ Status BaseClockTable::Insert(const ClockHandleBasicData& proto,
566
+ typename Table::HandleImpl** handle,
567
+ Cache::Priority priority, size_t capacity,
568
+ bool strict_capacity_limit) {
569
+ using HandleImpl = typename Table::HandleImpl;
570
+ Table& derived = static_cast<Table&>(*this);
571
+
572
+ typename Table::InsertState state;
573
+ derived.StartInsert(state);
574
+
398
575
  // Do we have the available occupancy? Optimistically assume we do
399
576
  // and deal with it if we don't.
400
577
  size_t old_occupancy = occupancy_.fetch_add(1, std::memory_order_acquire);
401
- auto revert_occupancy_fn = [&]() {
402
- occupancy_.fetch_sub(1, std::memory_order_relaxed);
403
- };
404
578
  // Whether we over-committed and need an eviction to make up for it
405
- bool need_evict_for_occupancy = old_occupancy >= occupancy_limit_;
579
+ bool need_evict_for_occupancy =
580
+ !derived.GrowIfNeeded(old_occupancy + 1, state);
406
581
 
407
582
  // Usage/capacity handling is somewhat different depending on
408
583
  // strict_capacity_limit, but mostly pessimistic.
409
584
  bool use_standalone_insert = false;
410
585
  const size_t total_charge = proto.GetTotalCharge();
411
586
  if (strict_capacity_limit) {
412
- Status s = ChargeUsageMaybeEvictStrict(total_charge, capacity,
413
- need_evict_for_occupancy);
587
+ Status s = ChargeUsageMaybeEvictStrict<Table>(
588
+ total_charge, capacity, need_evict_for_occupancy, state);
414
589
  if (!s.ok()) {
415
- revert_occupancy_fn();
590
+ // Revert occupancy
591
+ occupancy_.fetch_sub(1, std::memory_order_relaxed);
416
592
  return s;
417
593
  }
418
594
  } else {
419
595
  // Case strict_capacity_limit == false
420
- bool success = ChargeUsageMaybeEvictNonStrict(total_charge, capacity,
421
- need_evict_for_occupancy);
596
+ bool success = ChargeUsageMaybeEvictNonStrict<Table>(
597
+ total_charge, capacity, need_evict_for_occupancy, state);
422
598
  if (!success) {
423
- revert_occupancy_fn();
599
+ // Revert occupancy
600
+ occupancy_.fetch_sub(1, std::memory_order_relaxed);
424
601
  if (handle == nullptr) {
425
602
  // Don't insert the entry but still return ok, as if the entry
426
603
  // inserted into cache and evicted immediately.
@@ -433,11 +610,6 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
433
610
  }
434
611
  }
435
612
  }
436
- auto revert_usage_fn = [&]() {
437
- usage_.fetch_sub(total_charge, std::memory_order_relaxed);
438
- // No underflow
439
- assert(usage_.load(std::memory_order_relaxed) < SIZE_MAX / 2);
440
- };
441
613
 
442
614
  if (!use_standalone_insert) {
443
615
  // Attempt a table insert, but abort if we find an existing entry for the
@@ -451,129 +623,37 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
451
623
  uint64_t initial_countdown = GetInitialCountdown(priority);
452
624
  assert(initial_countdown > 0);
453
625
 
454
- size_t probe = 0;
455
- HandleImpl* e = FindSlot(
456
- proto.hashed_key,
457
- [&](HandleImpl* h) {
458
- // Optimistically transition the slot from "empty" to
459
- // "under construction" (no effect on other states)
460
- uint64_t old_meta =
461
- h->meta.fetch_or(uint64_t{ClockHandle::kStateOccupiedBit}
462
- << ClockHandle::kStateShift,
463
- std::memory_order_acq_rel);
464
- uint64_t old_state = old_meta >> ClockHandle::kStateShift;
465
-
466
- if (old_state == ClockHandle::kStateEmpty) {
467
- // We've started inserting into an available slot, and taken
468
- // ownership Save data fields
469
- ClockHandleBasicData* h_alias = h;
470
- *h_alias = proto;
471
-
472
- // Transition from "under construction" state to "visible" state
473
- uint64_t new_meta = uint64_t{ClockHandle::kStateVisible}
474
- << ClockHandle::kStateShift;
475
-
476
- // Maybe with an outstanding reference
477
- new_meta |= initial_countdown << ClockHandle::kAcquireCounterShift;
478
- new_meta |= (initial_countdown - (handle != nullptr))
479
- << ClockHandle::kReleaseCounterShift;
626
+ HandleImpl* e =
627
+ derived.DoInsert(proto, initial_countdown, handle != nullptr, state);
480
628
 
481
- #ifndef NDEBUG
482
- // Save the state transition, with assertion
483
- old_meta = h->meta.exchange(new_meta, std::memory_order_release);
484
- assert(old_meta >> ClockHandle::kStateShift ==
485
- ClockHandle::kStateConstruction);
486
- #else
487
- // Save the state transition
488
- h->meta.store(new_meta, std::memory_order_release);
489
- #endif
490
- return true;
491
- } else if (old_state != ClockHandle::kStateVisible) {
492
- // Slot not usable / touchable now
493
- return false;
494
- }
495
- // Existing, visible entry, which might be a match.
496
- // But first, we need to acquire a ref to read it. In fact, number of
497
- // refs for initial countdown, so that we boost the clock state if
498
- // this is a match.
499
- old_meta = h->meta.fetch_add(
500
- ClockHandle::kAcquireIncrement * initial_countdown,
501
- std::memory_order_acq_rel);
502
- // Like Lookup
503
- if ((old_meta >> ClockHandle::kStateShift) ==
504
- ClockHandle::kStateVisible) {
505
- // Acquired a read reference
506
- if (h->hashed_key == proto.hashed_key) {
507
- // Match. Release in a way that boosts the clock state
508
- old_meta = h->meta.fetch_add(
509
- ClockHandle::kReleaseIncrement * initial_countdown,
510
- std::memory_order_acq_rel);
511
- // Correct for possible (but rare) overflow
512
- CorrectNearOverflow(old_meta, h->meta);
513
- // Insert standalone instead (only if return handle needed)
514
- use_standalone_insert = true;
515
- return true;
516
- } else {
517
- // Mismatch. Pretend we never took the reference
518
- old_meta = h->meta.fetch_sub(
519
- ClockHandle::kAcquireIncrement * initial_countdown,
520
- std::memory_order_acq_rel);
521
- }
522
- } else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
523
- ClockHandle::kStateInvisible)) {
524
- // Pretend we never took the reference
525
- // WART: there's a tiny chance we release last ref to invisible
526
- // entry here. If that happens, we let eviction take care of it.
527
- old_meta = h->meta.fetch_sub(
528
- ClockHandle::kAcquireIncrement * initial_countdown,
529
- std::memory_order_acq_rel);
530
- } else {
531
- // For other states, incrementing the acquire counter has no effect
532
- // so we don't need to undo it.
533
- // Slot not usable / touchable now.
534
- }
535
- (void)old_meta;
536
- return false;
537
- },
538
- [&](HandleImpl* /*h*/) { return false; },
539
- [&](HandleImpl* h) {
540
- h->displacements.fetch_add(1, std::memory_order_relaxed);
541
- },
542
- probe);
543
- if (e == nullptr) {
544
- // Occupancy check and never abort FindSlot above should generally
545
- // prevent this, except it's theoretically possible for other threads
546
- // to evict and replace entries in the right order to hit every slot
547
- // when it is populated. Assuming random hashing, the chance of that
548
- // should be no higher than pow(kStrictLoadFactor, n) for n slots.
549
- // That should be infeasible for roughly n >= 256, so if this assertion
550
- // fails, that suggests something is going wrong.
551
- assert(GetTableSize() < 256);
552
- use_standalone_insert = true;
553
- }
554
- if (!use_standalone_insert) {
629
+ if (e) {
555
630
  // Successfully inserted
556
631
  if (handle) {
557
632
  *handle = e;
558
633
  }
559
634
  return Status::OK();
560
635
  }
561
- // Roll back table insertion
562
- Rollback(proto.hashed_key, e);
563
- revert_occupancy_fn();
636
+ // Not inserted
637
+ // Revert occupancy
638
+ occupancy_.fetch_sub(1, std::memory_order_relaxed);
564
639
  // Maybe fall back on standalone insert
565
640
  if (handle == nullptr) {
566
- revert_usage_fn();
641
+ // Revert usage
642
+ usage_.fetch_sub(total_charge, std::memory_order_relaxed);
643
+ // No underflow
644
+ assert(usage_.load(std::memory_order_relaxed) < SIZE_MAX / 2);
567
645
  // As if unrefed entry immdiately evicted
568
646
  proto.FreeData(allocator_);
569
647
  return Status::OK();
570
648
  }
649
+
650
+ use_standalone_insert = true;
571
651
  }
572
652
 
573
653
  // Run standalone insert
574
654
  assert(use_standalone_insert);
575
655
 
576
- *handle = StandaloneInsert(proto);
656
+ *handle = StandaloneInsert<HandleImpl>(proto);
577
657
 
578
658
  // The OkOverwritten status is used to count "redundant" insertions into
579
659
  // block cache. This implementation doesn't strictly check for redundant
@@ -583,49 +663,168 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
583
663
  return Status::OkOverwritten();
584
664
  }
585
665
 
586
- HyperClockTable::HandleImpl* HyperClockTable::CreateStandalone(
587
- ClockHandleBasicData& proto, size_t capacity, bool strict_capacity_limit,
588
- bool allow_uncharged) {
589
- const size_t total_charge = proto.GetTotalCharge();
590
- if (strict_capacity_limit) {
591
- Status s = ChargeUsageMaybeEvictStrict(total_charge, capacity,
592
- /*need_evict_for_occupancy=*/false);
593
- if (!s.ok()) {
594
- if (allow_uncharged) {
595
- proto.total_charge = 0;
596
- } else {
597
- return nullptr;
598
- }
599
- }
600
- } else {
601
- // Case strict_capacity_limit == false
602
- bool success =
603
- ChargeUsageMaybeEvictNonStrict(total_charge, capacity,
604
- /*need_evict_for_occupancy=*/false);
605
- if (!success) {
606
- // Force the issue
607
- usage_.fetch_add(total_charge, std::memory_order_relaxed);
608
- }
609
- }
666
+ void BaseClockTable::Ref(ClockHandle& h) {
667
+ // Increment acquire counter
668
+ uint64_t old_meta = h.meta.fetch_add(ClockHandle::kAcquireIncrement,
669
+ std::memory_order_acquire);
610
670
 
611
- return StandaloneInsert(proto);
671
+ assert((old_meta >> ClockHandle::kStateShift) &
672
+ ClockHandle::kStateShareableBit);
673
+ // Must have already had a reference
674
+ assert(GetRefcount(old_meta) > 0);
675
+ (void)old_meta;
612
676
  }
613
677
 
614
- HyperClockTable::HandleImpl* HyperClockTable::Lookup(
615
- const UniqueId64x2& hashed_key) {
616
- size_t probe = 0;
617
- HandleImpl* e = FindSlot(
618
- hashed_key,
619
- [&](HandleImpl* h) {
620
- // Mostly branch-free version (similar performance)
621
- /*
622
- uint64_t old_meta = h->meta.fetch_add(ClockHandle::kAcquireIncrement,
623
- std::memory_order_acquire);
624
- bool Shareable = (old_meta >> (ClockHandle::kStateShift + 1)) & 1U;
625
- bool visible = (old_meta >> ClockHandle::kStateShift) & 1U;
626
- bool match = (h->key == key) & visible;
627
- h->meta.fetch_sub(static_cast<uint64_t>(Shareable & !match) <<
628
- ClockHandle::kAcquireCounterShift, std::memory_order_release); return
678
+ #ifndef NDEBUG
679
+ void BaseClockTable::TEST_RefN(ClockHandle& h, size_t n) {
680
+ // Increment acquire counter
681
+ uint64_t old_meta = h.meta.fetch_add(n * ClockHandle::kAcquireIncrement,
682
+ std::memory_order_acquire);
683
+
684
+ assert((old_meta >> ClockHandle::kStateShift) &
685
+ ClockHandle::kStateShareableBit);
686
+ (void)old_meta;
687
+ }
688
+
689
+ void BaseClockTable::TEST_ReleaseNMinus1(ClockHandle* h, size_t n) {
690
+ assert(n > 0);
691
+
692
+ // Like n-1 Releases, but assumes one more will happen in the caller to take
693
+ // care of anything like erasing an unreferenced, invisible entry.
694
+ uint64_t old_meta = h->meta.fetch_add(
695
+ (n - 1) * ClockHandle::kReleaseIncrement, std::memory_order_acquire);
696
+ assert((old_meta >> ClockHandle::kStateShift) &
697
+ ClockHandle::kStateShareableBit);
698
+ (void)old_meta;
699
+ }
700
+ #endif
701
+
702
+ FixedHyperClockTable::FixedHyperClockTable(
703
+ size_t capacity, bool /*strict_capacity_limit*/,
704
+ CacheMetadataChargePolicy metadata_charge_policy,
705
+ MemoryAllocator* allocator,
706
+ const Cache::EvictionCallback* eviction_callback, const uint32_t* hash_seed,
707
+ const Opts& opts)
708
+ : BaseClockTable(metadata_charge_policy, allocator, eviction_callback,
709
+ hash_seed),
710
+ length_bits_(CalcHashBits(capacity, opts.estimated_value_size,
711
+ metadata_charge_policy)),
712
+ length_bits_mask_((size_t{1} << length_bits_) - 1),
713
+ occupancy_limit_(static_cast<size_t>((uint64_t{1} << length_bits_) *
714
+ kStrictLoadFactor)),
715
+ array_(new HandleImpl[size_t{1} << length_bits_]) {
716
+ if (metadata_charge_policy ==
717
+ CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
718
+ usage_ += size_t{GetTableSize()} * sizeof(HandleImpl);
719
+ }
720
+
721
+ static_assert(sizeof(HandleImpl) == 64U,
722
+ "Expecting size / alignment with common cache line size");
723
+ }
724
+
725
+ FixedHyperClockTable::~FixedHyperClockTable() {
726
+ // Assumes there are no references or active operations on any slot/element
727
+ // in the table.
728
+ for (size_t i = 0; i < GetTableSize(); i++) {
729
+ HandleImpl& h = array_[i];
730
+ switch (h.meta >> ClockHandle::kStateShift) {
731
+ case ClockHandle::kStateEmpty:
732
+ // noop
733
+ break;
734
+ case ClockHandle::kStateInvisible: // rare but possible
735
+ case ClockHandle::kStateVisible:
736
+ assert(GetRefcount(h.meta) == 0);
737
+ h.FreeData(allocator_);
738
+ #ifndef NDEBUG
739
+ Rollback(h.hashed_key, &h);
740
+ ReclaimEntryUsage(h.GetTotalCharge());
741
+ #endif
742
+ break;
743
+ // otherwise
744
+ default:
745
+ assert(false);
746
+ break;
747
+ }
748
+ }
749
+
750
+ #ifndef NDEBUG
751
+ for (size_t i = 0; i < GetTableSize(); i++) {
752
+ assert(array_[i].displacements.load() == 0);
753
+ }
754
+ #endif
755
+
756
+ assert(usage_.load() == 0 ||
757
+ usage_.load() == size_t{GetTableSize()} * sizeof(HandleImpl));
758
+ assert(occupancy_ == 0);
759
+ }
760
+
761
+ void FixedHyperClockTable::StartInsert(InsertState&) {}
762
+
763
+ bool FixedHyperClockTable::GrowIfNeeded(size_t new_occupancy, InsertState&) {
764
+ return new_occupancy <= occupancy_limit_;
765
+ }
766
+
767
+ FixedHyperClockTable::HandleImpl* FixedHyperClockTable::DoInsert(
768
+ const ClockHandleBasicData& proto, uint64_t initial_countdown,
769
+ bool keep_ref, InsertState&) {
770
+ bool already_matches = false;
771
+ HandleImpl* e = FindSlot(
772
+ proto.hashed_key,
773
+ [&](HandleImpl* h) {
774
+ return TryInsert(proto, *h, initial_countdown, keep_ref,
775
+ &already_matches);
776
+ },
777
+ [&](HandleImpl* h) {
778
+ if (already_matches) {
779
+ // Stop searching & roll back displacements
780
+ Rollback(proto.hashed_key, h);
781
+ return true;
782
+ } else {
783
+ // Keep going
784
+ return false;
785
+ }
786
+ },
787
+ [&](HandleImpl* h, bool is_last) {
788
+ if (is_last) {
789
+ // Search is ending. Roll back displacements
790
+ Rollback(proto.hashed_key, h);
791
+ } else {
792
+ h->displacements.fetch_add(1, std::memory_order_relaxed);
793
+ }
794
+ });
795
+ if (already_matches) {
796
+ // Insertion skipped
797
+ return nullptr;
798
+ }
799
+ if (e != nullptr) {
800
+ // Successfully inserted
801
+ return e;
802
+ }
803
+ // Else, no available slot found. Occupancy check should generally prevent
804
+ // this, except it's theoretically possible for other threads to evict and
805
+ // replace entries in the right order to hit every slot when it is populated.
806
+ // Assuming random hashing, the chance of that should be no higher than
807
+ // pow(kStrictLoadFactor, n) for n slots. That should be infeasible for
808
+ // roughly n >= 256, so if this assertion fails, that suggests something is
809
+ // going wrong.
810
+ assert(GetTableSize() < 256);
811
+ return nullptr;
812
+ }
813
+
814
+ FixedHyperClockTable::HandleImpl* FixedHyperClockTable::Lookup(
815
+ const UniqueId64x2& hashed_key) {
816
+ HandleImpl* e = FindSlot(
817
+ hashed_key,
818
+ [&](HandleImpl* h) {
819
+ // Mostly branch-free version (similar performance)
820
+ /*
821
+ uint64_t old_meta = h->meta.fetch_add(ClockHandle::kAcquireIncrement,
822
+ std::memory_order_acquire);
823
+ bool Shareable = (old_meta >> (ClockHandle::kStateShift + 1)) & 1U;
824
+ bool visible = (old_meta >> ClockHandle::kStateShift) & 1U;
825
+ bool match = (h->key == key) & visible;
826
+ h->meta.fetch_sub(static_cast<uint64_t>(Shareable & !match) <<
827
+ ClockHandle::kAcquireCounterShift, std::memory_order_release); return
629
828
  match;
630
829
  */
631
830
  // Optimistic lookup should pay off when the table is relatively
@@ -648,38 +847,38 @@ HyperClockTable::HandleImpl* HyperClockTable::Lookup(
648
847
  // Acquired a read reference
649
848
  if (h->hashed_key == hashed_key) {
650
849
  // Match
850
+ // Update the hit bit
851
+ if (eviction_callback_) {
852
+ h->meta.fetch_or(uint64_t{1} << ClockHandle::kHitBitShift,
853
+ std::memory_order_relaxed);
854
+ }
651
855
  return true;
652
856
  } else {
653
857
  // Mismatch. Pretend we never took the reference
654
- old_meta = h->meta.fetch_sub(ClockHandle::kAcquireIncrement,
655
- std::memory_order_release);
858
+ Unref(*h);
656
859
  }
657
860
  } else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
658
861
  ClockHandle::kStateInvisible)) {
659
862
  // Pretend we never took the reference
660
- // WART: there's a tiny chance we release last ref to invisible
661
- // entry here. If that happens, we let eviction take care of it.
662
- old_meta = h->meta.fetch_sub(ClockHandle::kAcquireIncrement,
663
- std::memory_order_release);
863
+ Unref(*h);
664
864
  } else {
665
865
  // For other states, incrementing the acquire counter has no effect
666
866
  // so we don't need to undo it. Furthermore, we cannot safely undo
667
867
  // it because we did not acquire a read reference to lock the
668
868
  // entry in a Shareable state.
669
869
  }
670
- (void)old_meta;
671
870
  return false;
672
871
  },
673
872
  [&](HandleImpl* h) {
674
873
  return h->displacements.load(std::memory_order_relaxed) == 0;
675
874
  },
676
- [&](HandleImpl* /*h*/) {}, probe);
875
+ [&](HandleImpl* /*h*/, bool /*is_last*/) {});
677
876
 
678
877
  return e;
679
878
  }
680
879
 
681
- bool HyperClockTable::Release(HandleImpl* h, bool useful,
682
- bool erase_if_last_ref) {
880
+ bool FixedHyperClockTable::Release(HandleImpl* h, bool useful,
881
+ bool erase_if_last_ref) {
683
882
  // In contrast with LRUCache's Release, this function won't delete the handle
684
883
  // when the cache is above capacity and the reference is the last one. Space
685
884
  // is only freed up by EvictFromClock (called by Insert when space is needed)
@@ -706,6 +905,9 @@ bool HyperClockTable::Release(HandleImpl* h, bool useful,
706
905
 
707
906
  if (erase_if_last_ref || UNLIKELY(old_meta >> ClockHandle::kStateShift ==
708
907
  ClockHandle::kStateInvisible)) {
908
+ // FIXME: There's a chance here that another thread could replace this
909
+ // entry and we end up erasing the wrong one.
910
+
709
911
  // Update for last fetch_add op
710
912
  if (useful) {
711
913
  old_meta += ClockHandle::kReleaseIncrement;
@@ -753,43 +955,19 @@ bool HyperClockTable::Release(HandleImpl* h, bool useful,
753
955
  }
754
956
  }
755
957
 
756
- void HyperClockTable::Ref(HandleImpl& h) {
757
- // Increment acquire counter
758
- uint64_t old_meta = h.meta.fetch_add(ClockHandle::kAcquireIncrement,
759
- std::memory_order_acquire);
760
-
761
- assert((old_meta >> ClockHandle::kStateShift) &
762
- ClockHandle::kStateShareableBit);
763
- // Must have already had a reference
764
- assert(GetRefcount(old_meta) > 0);
765
- (void)old_meta;
766
- }
767
-
768
- void HyperClockTable::TEST_RefN(HandleImpl& h, size_t n) {
769
- // Increment acquire counter
770
- uint64_t old_meta = h.meta.fetch_add(n * ClockHandle::kAcquireIncrement,
771
- std::memory_order_acquire);
772
-
773
- assert((old_meta >> ClockHandle::kStateShift) &
774
- ClockHandle::kStateShareableBit);
775
- (void)old_meta;
776
- }
777
-
778
- void HyperClockTable::TEST_ReleaseN(HandleImpl* h, size_t n) {
958
+ #ifndef NDEBUG
959
+ void FixedHyperClockTable::TEST_ReleaseN(HandleImpl* h, size_t n) {
779
960
  if (n > 0) {
780
- // Split into n - 1 and 1 steps.
781
- uint64_t old_meta = h->meta.fetch_add(
782
- (n - 1) * ClockHandle::kReleaseIncrement, std::memory_order_acquire);
783
- assert((old_meta >> ClockHandle::kStateShift) &
784
- ClockHandle::kStateShareableBit);
785
- (void)old_meta;
961
+ // Do n-1 simple releases first
962
+ TEST_ReleaseNMinus1(h, n);
786
963
 
964
+ // Then the last release might be more involved
787
965
  Release(h, /*useful*/ true, /*erase_if_last_ref*/ false);
788
966
  }
789
967
  }
968
+ #endif
790
969
 
791
- void HyperClockTable::Erase(const UniqueId64x2& hashed_key) {
792
- size_t probe = 0;
970
+ void FixedHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
793
971
  (void)FindSlot(
794
972
  hashed_key,
795
973
  [&](HandleImpl* h) {
@@ -816,8 +994,7 @@ void HyperClockTable::Erase(const UniqueId64x2& hashed_key) {
816
994
  if (refcount > 1) {
817
995
  // Not last ref at some point in time during this Erase call
818
996
  // Pretend we never took the reference
819
- h->meta.fetch_sub(ClockHandle::kAcquireIncrement,
820
- std::memory_order_release);
997
+ Unref(*h);
821
998
  break;
822
999
  } else if (h->meta.compare_exchange_weak(
823
1000
  old_meta,
@@ -837,16 +1014,12 @@ void HyperClockTable::Erase(const UniqueId64x2& hashed_key) {
837
1014
  }
838
1015
  } else {
839
1016
  // Mismatch. Pretend we never took the reference
840
- h->meta.fetch_sub(ClockHandle::kAcquireIncrement,
841
- std::memory_order_release);
1017
+ Unref(*h);
842
1018
  }
843
1019
  } else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
844
1020
  ClockHandle::kStateInvisible)) {
845
1021
  // Pretend we never took the reference
846
- // WART: there's a tiny chance we release last ref to invisible
847
- // entry here. If that happens, we let eviction take care of it.
848
- h->meta.fetch_sub(ClockHandle::kAcquireIncrement,
849
- std::memory_order_release);
1022
+ Unref(*h);
850
1023
  } else {
851
1024
  // For other states, incrementing the acquire counter has no effect
852
1025
  // so we don't need to undo it.
@@ -856,51 +1029,10 @@ void HyperClockTable::Erase(const UniqueId64x2& hashed_key) {
856
1029
  [&](HandleImpl* h) {
857
1030
  return h->displacements.load(std::memory_order_relaxed) == 0;
858
1031
  },
859
- [&](HandleImpl* /*h*/) {}, probe);
860
- }
861
-
862
- void HyperClockTable::ConstApplyToEntriesRange(
863
- std::function<void(const HandleImpl&)> func, size_t index_begin,
864
- size_t index_end, bool apply_if_will_be_deleted) const {
865
- uint64_t check_state_mask = ClockHandle::kStateShareableBit;
866
- if (!apply_if_will_be_deleted) {
867
- check_state_mask |= ClockHandle::kStateVisibleBit;
868
- }
869
-
870
- for (size_t i = index_begin; i < index_end; i++) {
871
- HandleImpl& h = array_[i];
872
-
873
- // Note: to avoid using compare_exchange, we have to be extra careful.
874
- uint64_t old_meta = h.meta.load(std::memory_order_relaxed);
875
- // Check if it's an entry visible to lookups
876
- if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
877
- // Increment acquire counter. Note: it's possible that the entry has
878
- // completely changed since we loaded old_meta, but incrementing acquire
879
- // count is always safe. (Similar to optimistic Lookup here.)
880
- old_meta = h.meta.fetch_add(ClockHandle::kAcquireIncrement,
881
- std::memory_order_acquire);
882
- // Check whether we actually acquired a reference.
883
- if ((old_meta >> ClockHandle::kStateShift) &
884
- ClockHandle::kStateShareableBit) {
885
- // Apply func if appropriate
886
- if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
887
- func(h);
888
- }
889
- // Pretend we never took the reference
890
- h.meta.fetch_sub(ClockHandle::kAcquireIncrement,
891
- std::memory_order_release);
892
- // No net change, so don't need to check for overflow
893
- } else {
894
- // For other states, incrementing the acquire counter has no effect
895
- // so we don't need to undo it. Furthermore, we cannot safely undo
896
- // it because we did not acquire a read reference to lock the
897
- // entry in a Shareable state.
898
- }
899
- }
900
- }
1032
+ [&](HandleImpl* /*h*/, bool /*is_last*/) {});
901
1033
  }
902
1034
 
903
- void HyperClockTable::EraseUnRefEntries() {
1035
+ void FixedHyperClockTable::EraseUnRefEntries() {
904
1036
  for (size_t i = 0; i <= this->length_bits_mask_; i++) {
905
1037
  HandleImpl& h = array_[i];
906
1038
 
@@ -921,10 +1053,10 @@ void HyperClockTable::EraseUnRefEntries() {
921
1053
  }
922
1054
  }
923
1055
 
924
- inline HyperClockTable::HandleImpl* HyperClockTable::FindSlot(
925
- const UniqueId64x2& hashed_key, std::function<bool(HandleImpl*)> match_fn,
926
- std::function<bool(HandleImpl*)> abort_fn,
927
- std::function<void(HandleImpl*)> update_fn, size_t& probe) {
1056
+ template <typename MatchFn, typename AbortFn, typename UpdateFn>
1057
+ inline FixedHyperClockTable::HandleImpl* FixedHyperClockTable::FindSlot(
1058
+ const UniqueId64x2& hashed_key, const MatchFn& match_fn,
1059
+ const AbortFn& abort_fn, const UpdateFn& update_fn) {
928
1060
  // NOTE: upper 32 bits of hashed_key[0] is used for sharding
929
1061
  //
930
1062
  // We use double-hashing probing. Every probe in the sequence is a
@@ -938,26 +1070,27 @@ inline HyperClockTable::HandleImpl* HyperClockTable::FindSlot(
938
1070
  // TODO: we could also reconsider linear probing, though locality benefits
939
1071
  // are limited because each slot is a full cache line
940
1072
  size_t increment = static_cast<size_t>(hashed_key[0]) | 1U;
941
- size_t current = ModTableSize(base + probe * increment);
942
- while (probe <= length_bits_mask_) {
1073
+ size_t first = ModTableSize(base);
1074
+ size_t current = first;
1075
+ bool is_last;
1076
+ do {
943
1077
  HandleImpl* h = &array_[current];
944
1078
  if (match_fn(h)) {
945
- probe++;
946
1079
  return h;
947
1080
  }
948
1081
  if (abort_fn(h)) {
949
1082
  return nullptr;
950
1083
  }
951
- probe++;
952
- update_fn(h);
953
1084
  current = ModTableSize(current + increment);
954
- }
1085
+ is_last = current == first;
1086
+ update_fn(h, is_last);
1087
+ } while (!is_last);
955
1088
  // We looped back.
956
1089
  return nullptr;
957
1090
  }
958
1091
 
959
- inline void HyperClockTable::Rollback(const UniqueId64x2& hashed_key,
960
- const HandleImpl* h) {
1092
+ inline void FixedHyperClockTable::Rollback(const UniqueId64x2& hashed_key,
1093
+ const HandleImpl* h) {
961
1094
  size_t current = ModTableSize(hashed_key[1]);
962
1095
  size_t increment = static_cast<size_t>(hashed_key[0]) | 1U;
963
1096
  while (&array_[current] != h) {
@@ -966,7 +1099,7 @@ inline void HyperClockTable::Rollback(const UniqueId64x2& hashed_key,
966
1099
  }
967
1100
  }
968
1101
 
969
- inline void HyperClockTable::ReclaimEntryUsage(size_t total_charge) {
1102
+ inline void FixedHyperClockTable::ReclaimEntryUsage(size_t total_charge) {
970
1103
  auto old_occupancy = occupancy_.fetch_sub(1U, std::memory_order_release);
971
1104
  (void)old_occupancy;
972
1105
  // No underflow
@@ -977,8 +1110,8 @@ inline void HyperClockTable::ReclaimEntryUsage(size_t total_charge) {
977
1110
  assert(old_usage >= total_charge);
978
1111
  }
979
1112
 
980
- inline void HyperClockTable::Evict(size_t requested_charge,
981
- size_t* freed_charge, size_t* freed_count) {
1113
+ inline void FixedHyperClockTable::Evict(size_t requested_charge, InsertState&,
1114
+ EvictionData* data) {
982
1115
  // precondition
983
1116
  assert(requested_charge > 0);
984
1117
 
@@ -997,33 +1130,18 @@ inline void HyperClockTable::Evict(size_t requested_charge,
997
1130
  uint64_t max_clock_pointer =
998
1131
  old_clock_pointer + (ClockHandle::kMaxCountdown << length_bits_);
999
1132
 
1000
- // For key reconstructed from hash
1001
- UniqueId64x2 unhashed;
1002
-
1003
1133
  for (;;) {
1004
1134
  for (size_t i = 0; i < step_size; i++) {
1005
1135
  HandleImpl& h = array_[ModTableSize(Lower32of64(old_clock_pointer + i))];
1006
1136
  bool evicting = ClockUpdate(h);
1007
1137
  if (evicting) {
1008
1138
  Rollback(h.hashed_key, &h);
1009
- *freed_charge += h.GetTotalCharge();
1010
- *freed_count += 1;
1011
- bool took_ownership = false;
1012
- if (eviction_callback_) {
1013
- took_ownership =
1014
- eviction_callback_(ClockCacheShard<HyperClockTable>::ReverseHash(
1015
- h.GetHash(), &unhashed, hash_seed_),
1016
- reinterpret_cast<Cache::Handle*>(&h));
1017
- }
1018
- if (!took_ownership) {
1019
- h.FreeData(allocator_);
1020
- }
1021
- MarkEmpty(h);
1139
+ TrackAndReleaseEvictedEntry(&h, data);
1022
1140
  }
1023
1141
  }
1024
1142
 
1025
1143
  // Loop exit condition
1026
- if (*freed_charge >= requested_charge) {
1144
+ if (data->freed_charge >= requested_charge) {
1027
1145
  return;
1028
1146
  }
1029
1147
  if (old_clock_pointer >= max_clock_pointer) {
@@ -1063,38 +1181,35 @@ void ClockCacheShard<Table>::ApplyToSomeEntries(
1063
1181
  size_t charge,
1064
1182
  const Cache::CacheItemHelper* helper)>& callback,
1065
1183
  size_t average_entries_per_lock, size_t* state) {
1066
- // The state is essentially going to be the starting hash, which works
1067
- // nicely even if we resize between calls because we use upper-most
1068
- // hash bits for table indexes.
1069
- size_t length_bits = table_.GetLengthBits();
1184
+ // The state will be a simple index into the table. Even with a dynamic
1185
+ // hyper clock cache, entries will generally stay in their existing
1186
+ // slots, so we don't need to be aware of the high-level organization
1187
+ // that makes lookup efficient.
1070
1188
  size_t length = table_.GetTableSize();
1071
1189
 
1072
1190
  assert(average_entries_per_lock > 0);
1073
- // Assuming we are called with same average_entries_per_lock repeatedly,
1074
- // this simplifies some logic (index_end will not overflow).
1075
- assert(average_entries_per_lock < length || *state == 0);
1076
1191
 
1077
- size_t index_begin = *state >> (sizeof(size_t) * 8u - length_bits);
1192
+ size_t index_begin = *state;
1078
1193
  size_t index_end = index_begin + average_entries_per_lock;
1079
1194
  if (index_end >= length) {
1080
1195
  // Going to end.
1081
1196
  index_end = length;
1082
1197
  *state = SIZE_MAX;
1083
1198
  } else {
1084
- *state = index_end << (sizeof(size_t) * 8u - length_bits);
1199
+ *state = index_end;
1085
1200
  }
1086
1201
 
1087
1202
  auto hash_seed = table_.GetHashSeed();
1088
- table_.ConstApplyToEntriesRange(
1203
+ ConstApplyToEntriesRange(
1089
1204
  [callback, hash_seed](const HandleImpl& h) {
1090
1205
  UniqueId64x2 unhashed;
1091
1206
  callback(ReverseHash(h.hashed_key, &unhashed, hash_seed), h.value,
1092
1207
  h.GetTotalCharge(), h.helper);
1093
1208
  },
1094
- index_begin, index_end, false);
1209
+ table_.HandlePtr(index_begin), table_.HandlePtr(index_end), false);
1095
1210
  }
1096
1211
 
1097
- int HyperClockTable::CalcHashBits(
1212
+ int FixedHyperClockTable::CalcHashBits(
1098
1213
  size_t capacity, size_t estimated_value_size,
1099
1214
  CacheMetadataChargePolicy metadata_charge_policy) {
1100
1215
  double average_slot_charge = estimated_value_size * kLoadFactor;
@@ -1146,18 +1261,15 @@ Status ClockCacheShard<Table>::Insert(const Slice& key,
1146
1261
  proto.value = value;
1147
1262
  proto.helper = helper;
1148
1263
  proto.total_charge = charge;
1149
- return table_.Insert(proto, handle, priority,
1150
- capacity_.load(std::memory_order_relaxed),
1151
- strict_capacity_limit_.load(std::memory_order_relaxed));
1264
+ return table_.template Insert<Table>(
1265
+ proto, handle, priority, capacity_.load(std::memory_order_relaxed),
1266
+ strict_capacity_limit_.load(std::memory_order_relaxed));
1152
1267
  }
1153
1268
 
1154
1269
  template <class Table>
1155
- typename ClockCacheShard<Table>::HandleImpl*
1156
- ClockCacheShard<Table>::CreateStandalone(const Slice& key,
1157
- const UniqueId64x2& hashed_key,
1158
- Cache::ObjectPtr obj,
1159
- const Cache::CacheItemHelper* helper,
1160
- size_t charge, bool allow_uncharged) {
1270
+ typename Table::HandleImpl* ClockCacheShard<Table>::CreateStandalone(
1271
+ const Slice& key, const UniqueId64x2& hashed_key, Cache::ObjectPtr obj,
1272
+ const Cache::CacheItemHelper* helper, size_t charge, bool allow_uncharged) {
1161
1273
  if (UNLIKELY(key.size() != kCacheKeySize)) {
1162
1274
  return nullptr;
1163
1275
  }
@@ -1166,7 +1278,7 @@ ClockCacheShard<Table>::CreateStandalone(const Slice& key,
1166
1278
  proto.value = obj;
1167
1279
  proto.helper = helper;
1168
1280
  proto.total_charge = charge;
1169
- return table_.CreateStandalone(
1281
+ return table_.template CreateStandalone<Table>(
1170
1282
  proto, capacity_.load(std::memory_order_relaxed),
1171
1283
  strict_capacity_limit_.load(std::memory_order_relaxed), allow_uncharged);
1172
1284
  }
@@ -1198,6 +1310,7 @@ bool ClockCacheShard<Table>::Release(HandleImpl* handle, bool useful,
1198
1310
  return table_.Release(handle, useful, erase_if_last_ref);
1199
1311
  }
1200
1312
 
1313
+ #ifndef NDEBUG
1201
1314
  template <class Table>
1202
1315
  void ClockCacheShard<Table>::TEST_RefN(HandleImpl* h, size_t n) {
1203
1316
  table_.TEST_RefN(*h, n);
@@ -1207,6 +1320,7 @@ template <class Table>
1207
1320
  void ClockCacheShard<Table>::TEST_ReleaseN(HandleImpl* h, size_t n) {
1208
1321
  table_.TEST_ReleaseN(h, n);
1209
1322
  }
1323
+ #endif
1210
1324
 
1211
1325
  template <class Table>
1212
1326
  bool ClockCacheShard<Table>::Release(HandleImpl* handle,
@@ -1249,7 +1363,7 @@ size_t ClockCacheShard<Table>::GetPinnedUsage() const {
1249
1363
  size_t table_pinned_usage = 0;
1250
1364
  const bool charge_metadata =
1251
1365
  metadata_charge_policy_ == kFullChargeCacheMetadata;
1252
- table_.ConstApplyToEntriesRange(
1366
+ ConstApplyToEntriesRange(
1253
1367
  [&table_pinned_usage, charge_metadata](const HandleImpl& h) {
1254
1368
  uint64_t meta = h.meta.load(std::memory_order_relaxed);
1255
1369
  uint64_t refcount = GetRefcount(meta);
@@ -1262,7 +1376,7 @@ size_t ClockCacheShard<Table>::GetPinnedUsage() const {
1262
1376
  }
1263
1377
  }
1264
1378
  },
1265
- 0, table_.GetTableSize(), true);
1379
+ table_.HandlePtr(0), table_.HandlePtr(table_.GetTableSize()), true);
1266
1380
 
1267
1381
  return table_pinned_usage + table_.GetStandaloneUsage();
1268
1382
  }
@@ -1283,36 +1397,40 @@ size_t ClockCacheShard<Table>::GetTableAddressCount() const {
1283
1397
  }
1284
1398
 
1285
1399
  // Explicit instantiation
1286
- template class ClockCacheShard<HyperClockTable>;
1400
+ template class ClockCacheShard<FixedHyperClockTable>;
1401
+ template class ClockCacheShard<AutoHyperClockTable>;
1287
1402
 
1288
- HyperClockCache::HyperClockCache(const HyperClockCacheOptions& opts)
1289
- : ShardedCache(opts) {
1290
- assert(opts.estimated_entry_charge > 0 ||
1291
- opts.metadata_charge_policy != kDontChargeCacheMetadata);
1403
+ template <class Table>
1404
+ BaseHyperClockCache<Table>::BaseHyperClockCache(
1405
+ const HyperClockCacheOptions& opts)
1406
+ : ShardedCache<ClockCacheShard<Table>>(opts) {
1292
1407
  // TODO: should not need to go through two levels of pointer indirection to
1293
1408
  // get to table entries
1294
- size_t per_shard = GetPerShardCapacity();
1409
+ size_t per_shard = this->GetPerShardCapacity();
1295
1410
  MemoryAllocator* alloc = this->memory_allocator();
1296
- InitShards([&](Shard* cs) {
1297
- HyperClockTable::Opts table_opts;
1298
- table_opts.estimated_value_size = opts.estimated_entry_charge;
1411
+ this->InitShards([&](Shard* cs) {
1412
+ typename Table::Opts table_opts{opts};
1299
1413
  new (cs) Shard(per_shard, opts.strict_capacity_limit,
1300
- opts.metadata_charge_policy, alloc, &eviction_callback_,
1301
- &hash_seed_, table_opts);
1414
+ opts.metadata_charge_policy, alloc,
1415
+ &this->eviction_callback_, &this->hash_seed_, table_opts);
1302
1416
  });
1303
1417
  }
1304
1418
 
1305
- Cache::ObjectPtr HyperClockCache::Value(Handle* handle) {
1306
- return reinterpret_cast<const HandleImpl*>(handle)->value;
1419
+ template <class Table>
1420
+ Cache::ObjectPtr BaseHyperClockCache<Table>::Value(Handle* handle) {
1421
+ return reinterpret_cast<const typename Table::HandleImpl*>(handle)->value;
1307
1422
  }
1308
1423
 
1309
- size_t HyperClockCache::GetCharge(Handle* handle) const {
1310
- return reinterpret_cast<const HandleImpl*>(handle)->GetTotalCharge();
1424
+ template <class Table>
1425
+ size_t BaseHyperClockCache<Table>::GetCharge(Handle* handle) const {
1426
+ return reinterpret_cast<const typename Table::HandleImpl*>(handle)
1427
+ ->GetTotalCharge();
1311
1428
  }
1312
1429
 
1313
- const Cache::CacheItemHelper* HyperClockCache::GetCacheItemHelper(
1430
+ template <class Table>
1431
+ const Cache::CacheItemHelper* BaseHyperClockCache<Table>::GetCacheItemHelper(
1314
1432
  Handle* handle) const {
1315
- auto h = reinterpret_cast<const HandleImpl*>(handle);
1433
+ auto h = reinterpret_cast<const typename Table::HandleImpl*>(handle);
1316
1434
  return h->helper;
1317
1435
  }
1318
1436
 
@@ -1325,7 +1443,7 @@ namespace {
1325
1443
  // or actual occupancy very close to limit (>95% of limit).
1326
1444
  // Also, for each shard compute the recommended estimated_entry_charge,
1327
1445
  // and keep the minimum one for use as overall recommendation.
1328
- void AddShardEvaluation(const HyperClockCache::Shard& shard,
1446
+ void AddShardEvaluation(const FixedHyperClockCache::Shard& shard,
1329
1447
  std::vector<double>& predicted_load_factors,
1330
1448
  size_t& min_recommendation) {
1331
1449
  size_t usage = shard.GetUsage() - shard.GetStandaloneUsage();
@@ -1343,7 +1461,7 @@ void AddShardEvaluation(const HyperClockCache::Shard& shard,
1343
1461
  // If filled to capacity, what would the occupancy ratio be?
1344
1462
  double ratio = occ_ratio / usage_ratio;
1345
1463
  // Given max load factor, what that load factor be?
1346
- double lf = ratio * kStrictLoadFactor;
1464
+ double lf = ratio * FixedHyperClockTable::kStrictLoadFactor;
1347
1465
  predicted_load_factors.push_back(lf);
1348
1466
 
1349
1467
  // Update min_recommendation also
@@ -1351,17 +1469,91 @@ void AddShardEvaluation(const HyperClockCache::Shard& shard,
1351
1469
  min_recommendation = std::min(min_recommendation, recommendation);
1352
1470
  }
1353
1471
 
1472
+ bool IsSlotOccupied(const ClockHandle& h) {
1473
+ return (h.meta.load(std::memory_order_relaxed) >> ClockHandle::kStateShift) !=
1474
+ 0;
1475
+ }
1354
1476
  } // namespace
1355
1477
 
1356
- void HyperClockCache::ReportProblems(
1478
+ // NOTE: GCC might warn about subobject linkage if this is in anon namespace
1479
+ template <size_t N = 500>
1480
+ class LoadVarianceStats {
1481
+ public:
1482
+ std::string Report() const {
1483
+ return "Overall " + PercentStr(positive_count_, samples_) + " (" +
1484
+ std::to_string(positive_count_) + "/" + std::to_string(samples_) +
1485
+ "), Min/Max/Window = " + PercentStr(min_, N) + "/" +
1486
+ PercentStr(max_, N) + "/" + std::to_string(N) +
1487
+ ", MaxRun{Pos/Neg} = " + std::to_string(max_pos_run_) + "/" +
1488
+ std::to_string(max_neg_run_);
1489
+ }
1490
+
1491
+ void Add(bool positive) {
1492
+ recent_[samples_ % N] = positive;
1493
+ if (positive) {
1494
+ ++positive_count_;
1495
+ ++cur_pos_run_;
1496
+ max_pos_run_ = std::max(max_pos_run_, cur_pos_run_);
1497
+ cur_neg_run_ = 0;
1498
+ } else {
1499
+ ++cur_neg_run_;
1500
+ max_neg_run_ = std::max(max_neg_run_, cur_neg_run_);
1501
+ cur_pos_run_ = 0;
1502
+ }
1503
+ ++samples_;
1504
+ if (samples_ >= N) {
1505
+ size_t count_set = recent_.count();
1506
+ max_ = std::max(max_, count_set);
1507
+ min_ = std::min(min_, count_set);
1508
+ }
1509
+ }
1510
+
1511
+ private:
1512
+ size_t max_ = 0;
1513
+ size_t min_ = N;
1514
+ size_t positive_count_ = 0;
1515
+ size_t samples_ = 0;
1516
+ size_t max_pos_run_ = 0;
1517
+ size_t cur_pos_run_ = 0;
1518
+ size_t max_neg_run_ = 0;
1519
+ size_t cur_neg_run_ = 0;
1520
+ std::bitset<N> recent_;
1521
+
1522
+ static std::string PercentStr(size_t a, size_t b) {
1523
+ if (b == 0) {
1524
+ return "??%";
1525
+ } else {
1526
+ return std::to_string(uint64_t{100} * a / b) + "%";
1527
+ }
1528
+ }
1529
+ };
1530
+
1531
+ template <class Table>
1532
+ void BaseHyperClockCache<Table>::ReportProblems(
1533
+ const std::shared_ptr<Logger>& info_log) const {
1534
+ if (info_log->GetInfoLogLevel() <= InfoLogLevel::DEBUG_LEVEL) {
1535
+ LoadVarianceStats slot_stats;
1536
+ this->ForEachShard([&](const BaseHyperClockCache<Table>::Shard* shard) {
1537
+ size_t count = shard->GetTableAddressCount();
1538
+ for (size_t i = 0; i < count; ++i) {
1539
+ slot_stats.Add(IsSlotOccupied(*shard->GetTable().HandlePtr(i)));
1540
+ }
1541
+ });
1542
+ ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
1543
+ "Slot occupancy stats: %s", slot_stats.Report().c_str());
1544
+ }
1545
+ }
1546
+
1547
+ void FixedHyperClockCache::ReportProblems(
1357
1548
  const std::shared_ptr<Logger>& info_log) const {
1549
+ BaseHyperClockCache::ReportProblems(info_log);
1550
+
1358
1551
  uint32_t shard_count = GetNumShards();
1359
1552
  std::vector<double> predicted_load_factors;
1360
1553
  size_t min_recommendation = SIZE_MAX;
1361
- const_cast<HyperClockCache*>(this)->ForEachShard(
1362
- [&](HyperClockCache::Shard* shard) {
1363
- AddShardEvaluation(*shard, predicted_load_factors, min_recommendation);
1364
- });
1554
+ ForEachShard([&](const FixedHyperClockCache::Shard* shard) {
1555
+ AddShardEvaluation(*shard, predicted_load_factors, min_recommendation);
1556
+ });
1365
1557
 
1366
1558
  if (predicted_load_factors.empty()) {
1367
1559
  // None operating "at limit" -> nothing to report
@@ -1382,17 +1574,19 @@ void HyperClockCache::ReportProblems(
1382
1574
  predicted_load_factors.end(), 0.0) /
1383
1575
  shard_count;
1384
1576
 
1385
- constexpr double kLowSpecLoadFactor = kLoadFactor / 2;
1386
- constexpr double kMidSpecLoadFactor = kLoadFactor / 1.414;
1387
- if (average_load_factor > kLoadFactor) {
1577
+ constexpr double kLowSpecLoadFactor = FixedHyperClockTable::kLoadFactor / 2;
1578
+ constexpr double kMidSpecLoadFactor =
1579
+ FixedHyperClockTable::kLoadFactor / 1.414;
1580
+ if (average_load_factor > FixedHyperClockTable::kLoadFactor) {
1388
1581
  // Out of spec => Consider reporting load factor too high
1389
1582
  // Estimate effective overall capacity loss due to enforcing occupancy limit
1390
1583
  double lost_portion = 0.0;
1391
1584
  int over_count = 0;
1392
1585
  for (double lf : predicted_load_factors) {
1393
- if (lf > kStrictLoadFactor) {
1586
+ if (lf > FixedHyperClockTable::kStrictLoadFactor) {
1394
1587
  ++over_count;
1395
- lost_portion += (lf - kStrictLoadFactor) / lf / shard_count;
1588
+ lost_portion +=
1589
+ (lf - FixedHyperClockTable::kStrictLoadFactor) / lf / shard_count;
1396
1590
  }
1397
1591
  }
1398
1592
  // >= 20% loss -> error
@@ -1416,10 +1610,10 @@ void HyperClockCache::ReportProblems(
1416
1610
  if (report) {
1417
1611
  ROCKS_LOG_AT_LEVEL(
1418
1612
  info_log, level,
1419
- "HyperClockCache@%p unable to use estimated %.1f%% capacity because "
1420
- "of "
1421
- "full occupancy in %d/%u cache shards (estimated_entry_charge too "
1422
- "high). Recommend estimated_entry_charge=%zu",
1613
+ "FixedHyperClockCache@%p unable to use estimated %.1f%% capacity "
1614
+ "because of full occupancy in %d/%u cache shards "
1615
+ "(estimated_entry_charge too high). "
1616
+ "Recommend estimated_entry_charge=%zu",
1423
1617
  this, lost_portion * 100.0, over_count, (unsigned)shard_count,
1424
1618
  min_recommendation);
1425
1619
  }
@@ -1437,41 +1631,2034 @@ void HyperClockCache::ReportProblems(
1437
1631
  }
1438
1632
  ROCKS_LOG_AT_LEVEL(
1439
1633
  info_log, level,
1440
- "HyperClockCache@%p table has low occupancy at full capacity. Higher "
1441
- "estimated_entry_charge (about %.1fx) would likely improve "
1634
+ "FixedHyperClockCache@%p table has low occupancy at full capacity. "
1635
+ "Higher estimated_entry_charge (about %.1fx) would likely improve "
1442
1636
  "performance. Recommend estimated_entry_charge=%zu",
1443
1637
  this, kMidSpecLoadFactor / average_load_factor, min_recommendation);
1444
1638
  }
1445
1639
  }
1446
1640
  }
1447
1641
 
1448
- } // namespace clock_cache
1642
+ // =======================================================================
1643
+ // AutoHyperClockCache
1644
+ // =======================================================================
1449
1645
 
1450
- // DEPRECATED (see public API)
1451
- std::shared_ptr<Cache> NewClockCache(
1452
- size_t capacity, int num_shard_bits, bool strict_capacity_limit,
1453
- CacheMetadataChargePolicy metadata_charge_policy) {
1454
- return NewLRUCache(capacity, num_shard_bits, strict_capacity_limit,
1455
- /* high_pri_pool_ratio */ 0.5, nullptr,
1456
- kDefaultToAdaptiveMutex, metadata_charge_policy,
1457
- /* low_pri_pool_ratio */ 0.0);
1646
+ // See AutoHyperClockTable::length_info_ etc. for how the linear hashing
1647
+ // metadata is encoded. Here are some example values:
1648
+ //
1649
+ // Used length | min shift | threshold | max shift
1650
+ // 2 | 1 | 0 | 1
1651
+ // 3 | 1 | 1 | 2
1652
+ // 4 | 2 | 0 | 2
1653
+ // 5 | 2 | 1 | 3
1654
+ // 6 | 2 | 2 | 3
1655
+ // 7 | 2 | 3 | 3
1656
+ // 8 | 3 | 0 | 3
1657
+ // 9 | 3 | 1 | 4
1658
+ // ...
1659
+ // Note:
1660
+ // * min shift = floor(log2(used length))
1661
+ // * max shift = ceil(log2(used length))
1662
+ // * used length == (1 << shift) + threshold
1663
+ // Also, shift=0 is never used in practice, so is reserved for "unset"
1664
+
1665
+ namespace {
1666
+
1667
+ inline int LengthInfoToMinShift(uint64_t length_info) {
1668
+ int mask_shift = BitwiseAnd(length_info, int{255});
1669
+ assert(mask_shift <= 63);
1670
+ assert(mask_shift > 0);
1671
+ return mask_shift;
1458
1672
  }
1459
1673
 
1460
- std::shared_ptr<Cache> HyperClockCacheOptions::MakeSharedCache() const {
1461
- // For sanitized options
1462
- HyperClockCacheOptions opts = *this;
1463
- if (opts.num_shard_bits >= 20) {
1464
- return nullptr; // The cache cannot be sharded into too many fine pieces.
1674
+ inline size_t LengthInfoToThreshold(uint64_t length_info) {
1675
+ return static_cast<size_t>(length_info >> 8);
1676
+ }
1677
+
1678
+ inline size_t LengthInfoToUsedLength(uint64_t length_info) {
1679
+ size_t threshold = LengthInfoToThreshold(length_info);
1680
+ int shift = LengthInfoToMinShift(length_info);
1681
+ assert(threshold < (size_t{1} << shift));
1682
+ size_t used_length = (size_t{1} << shift) + threshold;
1683
+ assert(used_length >= 2);
1684
+ return used_length;
1685
+ }
1686
+
1687
+ inline uint64_t UsedLengthToLengthInfo(size_t used_length) {
1688
+ assert(used_length >= 2);
1689
+ int shift = FloorLog2(used_length);
1690
+ uint64_t threshold = BottomNBits(used_length, shift);
1691
+ uint64_t length_info =
1692
+ (uint64_t{threshold} << 8) + static_cast<uint64_t>(shift);
1693
+ assert(LengthInfoToUsedLength(length_info) == used_length);
1694
+ assert(LengthInfoToMinShift(length_info) == shift);
1695
+ assert(LengthInfoToThreshold(length_info) == threshold);
1696
+ return length_info;
1697
+ }
1698
+
1699
+ inline size_t GetStartingLength(size_t capacity) {
1700
+ if (capacity > port::kPageSize) {
1701
+ // Start with one memory page
1702
+ return port::kPageSize / sizeof(AutoHyperClockTable::HandleImpl);
1703
+ } else {
1704
+ // Mostly to make unit tests happy
1705
+ return 4;
1465
1706
  }
1466
- if (opts.num_shard_bits < 0) {
1467
- // Use larger shard size to reduce risk of large entries clustering
1468
- // or skewing individual shards.
1469
- constexpr size_t min_shard_size = 32U * 1024U * 1024U;
1470
- opts.num_shard_bits =
1471
- GetDefaultCacheShardBits(opts.capacity, min_shard_size);
1707
+ }
1708
+
1709
+ inline size_t GetHomeIndex(uint64_t hash, int shift) {
1710
+ return static_cast<size_t>(BottomNBits(hash, shift));
1711
+ }
1712
+
1713
+ inline void GetHomeIndexAndShift(uint64_t length_info, uint64_t hash,
1714
+ size_t* home, int* shift) {
1715
+ int min_shift = LengthInfoToMinShift(length_info);
1716
+ size_t threshold = LengthInfoToThreshold(length_info);
1717
+ bool extra_shift = GetHomeIndex(hash, min_shift) < threshold;
1718
+ *home = GetHomeIndex(hash, min_shift + extra_shift);
1719
+ *shift = min_shift + extra_shift;
1720
+ assert(*home < LengthInfoToUsedLength(length_info));
1721
+ }
1722
+
1723
+ inline int GetShiftFromNextWithShift(uint64_t next_with_shift) {
1724
+ return BitwiseAnd(next_with_shift,
1725
+ AutoHyperClockTable::HandleImpl::kShiftMask);
1726
+ }
1727
+
1728
+ inline size_t GetNextFromNextWithShift(uint64_t next_with_shift) {
1729
+ return static_cast<size_t>(next_with_shift >>
1730
+ AutoHyperClockTable::HandleImpl::kNextShift);
1731
+ }
1732
+
1733
+ inline uint64_t MakeNextWithShift(size_t next, int shift) {
1734
+ return (uint64_t{next} << AutoHyperClockTable::HandleImpl::kNextShift) |
1735
+ static_cast<uint64_t>(shift);
1736
+ }
1737
+
1738
+ inline uint64_t MakeNextWithShiftEnd(size_t head, int shift) {
1739
+ return AutoHyperClockTable::HandleImpl::kNextEndFlags |
1740
+ MakeNextWithShift(head, shift);
1741
+ }
1742
+
1743
+ // Helper function for Lookup
1744
+ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
1745
+ int shift = 0, size_t home = 0,
1746
+ bool* full_match_or_unknown = nullptr) {
1747
+ // Must be at least something to match
1748
+ assert(hashed_key || shift > 0);
1749
+
1750
+ uint64_t old_meta;
1751
+ // (Optimistically) increment acquire counter.
1752
+ old_meta = h.meta.fetch_add(ClockHandle::kAcquireIncrement,
1753
+ std::memory_order_acquire);
1754
+ // Check if it's a referencable (sharable) entry
1755
+ if ((old_meta & (uint64_t{ClockHandle::kStateShareableBit}
1756
+ << ClockHandle::kStateShift)) == 0) {
1757
+ // For non-sharable states, incrementing the acquire counter has no effect
1758
+ // so we don't need to undo it. Furthermore, we cannot safely undo
1759
+ // it because we did not acquire a read reference to lock the
1760
+ // entry in a Shareable state.
1761
+ if (full_match_or_unknown) {
1762
+ *full_match_or_unknown = true;
1763
+ }
1764
+ return false;
1765
+ }
1766
+ // Else acquired a read reference
1767
+ assert(GetRefcount(old_meta + ClockHandle::kAcquireIncrement) > 0);
1768
+ if (hashed_key && h.hashed_key == *hashed_key &&
1769
+ LIKELY(old_meta & (uint64_t{ClockHandle::kStateVisibleBit}
1770
+ << ClockHandle::kStateShift))) {
1771
+ // Match on full key, visible
1772
+ if (full_match_or_unknown) {
1773
+ *full_match_or_unknown = true;
1774
+ }
1775
+ return true;
1776
+ } else if (shift > 0 && home == BottomNBits(h.hashed_key[1], shift)) {
1777
+ // NOTE: upper 32 bits of hashed_key[0] is used for sharding
1778
+ // Match on home address, possibly invisible
1779
+ if (full_match_or_unknown) {
1780
+ *full_match_or_unknown = false;
1781
+ }
1782
+ return true;
1783
+ } else {
1784
+ // Mismatch. Pretend we never took the reference
1785
+ Unref(h);
1786
+ if (full_match_or_unknown) {
1787
+ *full_match_or_unknown = false;
1788
+ }
1789
+ return false;
1790
+ }
1791
+ }
1792
+
1793
+ // Assumes a chain rewrite lock prevents concurrent modification of
1794
+ // these chain pointers
1795
+ void UpgradeShiftsOnRange(AutoHyperClockTable::HandleImpl* arr,
1796
+ size_t& frontier, uint64_t stop_before_or_new_tail,
1797
+ int old_shift, int new_shift) {
1798
+ assert(frontier != SIZE_MAX);
1799
+ assert(new_shift == old_shift + 1);
1800
+ (void)old_shift;
1801
+ (void)new_shift;
1802
+ using HandleImpl = AutoHyperClockTable::HandleImpl;
1803
+ for (;;) {
1804
+ uint64_t next_with_shift =
1805
+ arr[frontier].chain_next_with_shift.load(std::memory_order_acquire);
1806
+ assert(GetShiftFromNextWithShift(next_with_shift) == old_shift);
1807
+ if (next_with_shift == stop_before_or_new_tail) {
1808
+ // Stopping at entry with pointer matching "stop before"
1809
+ assert(!HandleImpl::IsEnd(next_with_shift));
1810
+ return;
1811
+ }
1812
+ if (HandleImpl::IsEnd(next_with_shift)) {
1813
+ // Also update tail to new tail
1814
+ assert(HandleImpl::IsEnd(stop_before_or_new_tail));
1815
+ arr[frontier].chain_next_with_shift.store(stop_before_or_new_tail,
1816
+ std::memory_order_release);
1817
+ // Mark nothing left to upgrade
1818
+ frontier = SIZE_MAX;
1819
+ return;
1820
+ }
1821
+ // Next is another entry to process, so upgrade and advance frontier
1822
+ arr[frontier].chain_next_with_shift.fetch_add(1U,
1823
+ std::memory_order_acq_rel);
1824
+ assert(GetShiftFromNextWithShift(next_with_shift + 1) == new_shift);
1825
+ frontier = GetNextFromNextWithShift(next_with_shift);
1826
+ }
1827
+ }
1828
+
1829
+ size_t CalcOccupancyLimit(size_t used_length) {
1830
+ return static_cast<size_t>(used_length * AutoHyperClockTable::kMaxLoadFactor +
1831
+ 0.999);
1832
+ }
1833
+
1834
+ } // namespace
1835
+
1836
+ // An RAII wrapper for locking a chain of entries (flag bit on the head)
1837
+ // so that there is only one thread allowed to remove entries from the
1838
+ // chain, or to rewrite it by splitting for Grow. Without the lock,
1839
+ // all lookups and insertions at the head can proceed wait-free.
1840
+ // The class also provides functions for safely manipulating the head pointer
1841
+ // while holding the lock--or wanting to should it become non-empty.
1842
+ //
1843
+ // The flag bits on the head are such that the head cannot be locked if it
1844
+ // is an empty chain, so that a "blind" fetch_or will try to lock a non-empty
1845
+ // chain but have no effect on an empty chain. When a potential rewrite
1846
+ // operation see an empty head pointer, there is no need to lock as the
1847
+ // operation is a no-op. However, there are some cases such as CAS-update
1848
+ // where locking might be required after initially not being needed, if the
1849
+ // operation is forced to revisit the head pointer.
1850
+ class AutoHyperClockTable::ChainRewriteLock {
1851
+ public:
1852
+ using HandleImpl = AutoHyperClockTable::HandleImpl;
1853
+ explicit ChainRewriteLock(HandleImpl* h, std::atomic<uint64_t>& yield_count,
1854
+ bool already_locked_or_end = false)
1855
+ : head_ptr_(&h->head_next_with_shift) {
1856
+ if (already_locked_or_end) {
1857
+ new_head_ = head_ptr_->load(std::memory_order_acquire);
1858
+ // already locked or end
1859
+ assert(new_head_ & HandleImpl::kHeadLocked);
1860
+ return;
1861
+ }
1862
+ Acquire(yield_count);
1863
+ }
1864
+
1865
+ ~ChainRewriteLock() {
1866
+ if (!IsEnd()) {
1867
+ // Release lock
1868
+ uint64_t old = head_ptr_->fetch_and(~HandleImpl::kHeadLocked,
1869
+ std::memory_order_release);
1870
+ (void)old;
1871
+ assert((old & HandleImpl::kNextEndFlags) == HandleImpl::kHeadLocked);
1872
+ }
1873
+ }
1874
+
1875
+ void Reset(HandleImpl* h, std::atomic<uint64_t>& yield_count) {
1876
+ this->~ChainRewriteLock();
1877
+ new (this) ChainRewriteLock(h, yield_count);
1878
+ }
1879
+
1880
+ // Expected current state, assuming no parallel updates.
1881
+ uint64_t GetNewHead() const { return new_head_; }
1882
+
1883
+ // Only safe if we know that the value hasn't changed from other threads
1884
+ void SimpleUpdate(uint64_t next_with_shift) {
1885
+ assert(head_ptr_->load(std::memory_order_acquire) == new_head_);
1886
+ new_head_ = next_with_shift | HandleImpl::kHeadLocked;
1887
+ head_ptr_->store(new_head_, std::memory_order_release);
1888
+ }
1889
+
1890
+ bool CasUpdate(uint64_t next_with_shift, std::atomic<uint64_t>& yield_count) {
1891
+ uint64_t new_head = next_with_shift | HandleImpl::kHeadLocked;
1892
+ uint64_t expected = GetNewHead();
1893
+ bool success = head_ptr_->compare_exchange_strong(
1894
+ expected, new_head, std::memory_order_acq_rel);
1895
+ if (success) {
1896
+ // Ensure IsEnd() is kept up-to-date, including for dtor
1897
+ new_head_ = new_head;
1898
+ } else {
1899
+ // Parallel update to head, such as Insert()
1900
+ if (IsEnd()) {
1901
+ // Didn't previously hold a lock
1902
+ if (HandleImpl::IsEnd(expected)) {
1903
+ // Still don't need to
1904
+ new_head_ = expected;
1905
+ } else {
1906
+ // Need to acquire lock before proceeding
1907
+ Acquire(yield_count);
1908
+ }
1909
+ } else {
1910
+ // Parallel update must preserve our lock
1911
+ assert((expected & HandleImpl::kNextEndFlags) ==
1912
+ HandleImpl::kHeadLocked);
1913
+ new_head_ = expected;
1914
+ }
1915
+ }
1916
+ return success;
1917
+ }
1918
+
1919
+ bool IsEnd() const { return HandleImpl::IsEnd(new_head_); }
1920
+
1921
+ private:
1922
+ void Acquire(std::atomic<uint64_t>& yield_count) {
1923
+ for (;;) {
1924
+ // Acquire removal lock on the chain
1925
+ uint64_t old_head = head_ptr_->fetch_or(HandleImpl::kHeadLocked,
1926
+ std::memory_order_acq_rel);
1927
+ if ((old_head & HandleImpl::kNextEndFlags) != HandleImpl::kHeadLocked) {
1928
+ // Either acquired the lock or lock not needed (end)
1929
+ assert((old_head & HandleImpl::kNextEndFlags) == 0 ||
1930
+ (old_head & HandleImpl::kNextEndFlags) ==
1931
+ HandleImpl::kNextEndFlags);
1932
+
1933
+ new_head_ = old_head | HandleImpl::kHeadLocked;
1934
+ break;
1935
+ }
1936
+ // NOTE: one of the few yield-wait loops, which is rare enough in practice
1937
+ // for its performance to be insignificant. (E.g. using C++20 atomic
1938
+ // wait/notify would likely be worse because of wasted notify costs.)
1939
+ yield_count.fetch_add(1, std::memory_order_relaxed);
1940
+ std::this_thread::yield();
1941
+ }
1942
+ }
1943
+
1944
+ std::atomic<uint64_t>* head_ptr_;
1945
+ uint64_t new_head_;
1946
+ };
1947
+
1948
+ AutoHyperClockTable::AutoHyperClockTable(
1949
+ size_t capacity, bool /*strict_capacity_limit*/,
1950
+ CacheMetadataChargePolicy metadata_charge_policy,
1951
+ MemoryAllocator* allocator,
1952
+ const Cache::EvictionCallback* eviction_callback, const uint32_t* hash_seed,
1953
+ const Opts& opts)
1954
+ : BaseClockTable(metadata_charge_policy, allocator, eviction_callback,
1955
+ hash_seed),
1956
+ array_(MemMapping::AllocateLazyZeroed(
1957
+ sizeof(HandleImpl) * CalcMaxUsableLength(capacity,
1958
+ opts.min_avg_value_size,
1959
+ metadata_charge_policy))),
1960
+ length_info_(UsedLengthToLengthInfo(GetStartingLength(capacity))),
1961
+ occupancy_limit_(
1962
+ CalcOccupancyLimit(LengthInfoToUsedLength(length_info_.load()))),
1963
+ clock_pointer_mask_(
1964
+ BottomNBits(UINT64_MAX, LengthInfoToMinShift(length_info_.load()))) {
1965
+ if (metadata_charge_policy ==
1966
+ CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
1967
+ // NOTE: ignoring page boundaries for simplicity
1968
+ usage_ += size_t{GetTableSize()} * sizeof(HandleImpl);
1969
+ }
1970
+
1971
+ static_assert(sizeof(HandleImpl) == 64U,
1972
+ "Expecting size / alignment with common cache line size");
1973
+
1974
+ // Populate head pointers
1975
+ uint64_t length_info = length_info_.load();
1976
+ int min_shift = LengthInfoToMinShift(length_info);
1977
+ int max_shift = min_shift + 1;
1978
+ size_t major = uint64_t{1} << min_shift;
1979
+ size_t used_length = GetTableSize();
1980
+
1981
+ assert(major <= used_length);
1982
+ assert(used_length <= major * 2);
1983
+
1984
+ // Initialize the initial usable set of slots. This slightly odd iteration
1985
+ // order makes it easier to get the correct shift amount on each head.
1986
+ for (size_t i = 0; i < major; ++i) {
1987
+ #ifndef NDEBUG
1988
+ int shift;
1989
+ size_t home;
1990
+ #endif
1991
+ if (major + i < used_length) {
1992
+ array_[i].head_next_with_shift = MakeNextWithShiftEnd(i, max_shift);
1993
+ array_[major + i].head_next_with_shift =
1994
+ MakeNextWithShiftEnd(major + i, max_shift);
1995
+ #ifndef NDEBUG // Extra invariant checking
1996
+ GetHomeIndexAndShift(length_info, i, &home, &shift);
1997
+ assert(home == i);
1998
+ assert(shift == max_shift);
1999
+ GetHomeIndexAndShift(length_info, major + i, &home, &shift);
2000
+ assert(home == major + i);
2001
+ assert(shift == max_shift);
2002
+ #endif
2003
+ } else {
2004
+ array_[i].head_next_with_shift = MakeNextWithShiftEnd(i, min_shift);
2005
+ #ifndef NDEBUG // Extra invariant checking
2006
+ GetHomeIndexAndShift(length_info, i, &home, &shift);
2007
+ assert(home == i);
2008
+ assert(shift == min_shift);
2009
+ GetHomeIndexAndShift(length_info, major + i, &home, &shift);
2010
+ assert(home == i);
2011
+ assert(shift == min_shift);
2012
+ #endif
2013
+ }
2014
+ }
2015
+ }
2016
+
2017
+ AutoHyperClockTable::~AutoHyperClockTable() {
2018
+ // As usual, destructor assumes there are no references or active operations
2019
+ // on any slot/element in the table.
2020
+
2021
+ // It's possible that there were not enough Insert() after final concurrent
2022
+ // Grow to ensure length_info_ (published GetTableSize()) is fully up to
2023
+ // date. Probe for first unused slot to ensure we see the whole structure.
2024
+ size_t used_end = GetTableSize();
2025
+ while (used_end < array_.Count() &&
2026
+ array_[used_end].head_next_with_shift.load() !=
2027
+ HandleImpl::kUnusedMarker) {
2028
+ used_end++;
2029
+ }
2030
+ #ifndef NDEBUG
2031
+ for (size_t i = used_end; i < array_.Count(); i++) {
2032
+ assert(array_[i].head_next_with_shift.load() == 0);
2033
+ assert(array_[i].chain_next_with_shift.load() == 0);
2034
+ assert(array_[i].meta.load() == 0);
2035
+ }
2036
+ std::vector<bool> was_populated(used_end);
2037
+ std::vector<bool> was_pointed_to(used_end);
2038
+ #endif
2039
+ for (size_t i = 0; i < used_end; i++) {
2040
+ HandleImpl& h = array_[i];
2041
+ switch (h.meta >> ClockHandle::kStateShift) {
2042
+ case ClockHandle::kStateEmpty:
2043
+ // noop
2044
+ break;
2045
+ case ClockHandle::kStateInvisible: // rare but possible
2046
+ case ClockHandle::kStateVisible:
2047
+ assert(GetRefcount(h.meta) == 0);
2048
+ h.FreeData(allocator_);
2049
+ #ifndef NDEBUG // Extra invariant checking
2050
+ usage_.fetch_sub(h.total_charge, std::memory_order_relaxed);
2051
+ occupancy_.fetch_sub(1U, std::memory_order_relaxed);
2052
+ was_populated[i] = true;
2053
+ if (!HandleImpl::IsEnd(h.chain_next_with_shift)) {
2054
+ assert((h.chain_next_with_shift & HandleImpl::kHeadLocked) == 0);
2055
+ size_t next = GetNextFromNextWithShift(h.chain_next_with_shift);
2056
+ assert(!was_pointed_to[next]);
2057
+ was_pointed_to[next] = true;
2058
+ }
2059
+ #endif
2060
+ break;
2061
+ // otherwise
2062
+ default:
2063
+ assert(false);
2064
+ break;
2065
+ }
2066
+ #ifndef NDEBUG // Extra invariant checking
2067
+ if (!HandleImpl::IsEnd(h.head_next_with_shift)) {
2068
+ size_t next = GetNextFromNextWithShift(h.head_next_with_shift);
2069
+ assert(!was_pointed_to[next]);
2070
+ was_pointed_to[next] = true;
2071
+ }
2072
+ #endif
2073
+ }
2074
+ #ifndef NDEBUG // Extra invariant checking
2075
+ // This check is not perfect, but should detect most reasonable cases
2076
+ // of abandonned or floating entries, etc. (A floating cycle would not
2077
+ // be reported as bad.)
2078
+ for (size_t i = 0; i < used_end; i++) {
2079
+ if (was_populated[i]) {
2080
+ assert(was_pointed_to[i]);
2081
+ } else {
2082
+ assert(!was_pointed_to[i]);
2083
+ }
2084
+ }
2085
+ #endif
2086
+
2087
+ // Metadata charging only follows the published table size
2088
+ assert(usage_.load() == 0 ||
2089
+ usage_.load() == GetTableSize() * sizeof(HandleImpl));
2090
+ assert(occupancy_ == 0);
2091
+ }
2092
+
2093
+ size_t AutoHyperClockTable::GetTableSize() const {
2094
+ return LengthInfoToUsedLength(length_info_.load(std::memory_order_acquire));
2095
+ }
2096
+
2097
+ size_t AutoHyperClockTable::GetOccupancyLimit() const {
2098
+ return occupancy_limit_.load(std::memory_order_acquire);
2099
+ }
2100
+
2101
+ void AutoHyperClockTable::StartInsert(InsertState& state) {
2102
+ state.saved_length_info = length_info_.load(std::memory_order_acquire);
2103
+ }
2104
+
2105
+ // Because we have linked lists, bugs or even hardware errors can make it
2106
+ // possible to create a cycle, which would lead to infinite loop.
2107
+ // Furthermore, when we have retry cases in the code, we want to be sure
2108
+ // these are not (and do not become) spin-wait loops. Given the assumption
2109
+ // of quality hashing and the infeasibility of consistently recurring
2110
+ // concurrent modifications to an entry or chain, we can safely bound the
2111
+ // number of loop iterations in feasible operation, whether following chain
2112
+ // pointers or retrying with some backtracking. A smaller limit is used for
2113
+ // stress testing, to detect potential issues such as cycles or spin-waits,
2114
+ // and a larger limit is used to break cycles should they occur in production.
2115
+ #define CHECK_TOO_MANY_ITERATIONS(i) \
2116
+ { \
2117
+ assert(i < 768); \
2118
+ if (UNLIKELY(i >= 4096)) { \
2119
+ std::terminate(); \
2120
+ } \
2121
+ }
2122
+
2123
+ bool AutoHyperClockTable::GrowIfNeeded(size_t new_occupancy,
2124
+ InsertState& state) {
2125
+ // new_occupancy has taken into account other threads that are also trying
2126
+ // to insert, so as soon as we see sufficient *published* usable size, we
2127
+ // can declare success even if we aren't the one that grows the table.
2128
+ // However, there's an awkward state where other threads own growing the
2129
+ // table to sufficient usable size, but the udpated size is not yet
2130
+ // published. If we wait, then that likely slows the ramp-up cache
2131
+ // performance. If we unblock ourselves by ensure we grow by at least one
2132
+ // slot, we could technically overshoot required size by number of parallel
2133
+ // threads accessing block cache. On balance considering typical cases and
2134
+ // the modest consequences of table being slightly too large, the latter
2135
+ // seems preferable.
2136
+ //
2137
+ // So if the published occupancy limit is too small, we unblock ourselves
2138
+ // by committing to growing the table by at least one slot. Also note that
2139
+ // we might need to grow more than once to actually increase the occupancy
2140
+ // limit (due to max load factor < 1.0)
2141
+
2142
+ while (UNLIKELY(new_occupancy >
2143
+ occupancy_limit_.load(std::memory_order_relaxed))) {
2144
+ // At this point we commit the thread to growing unless we've reached the
2145
+ // limit (returns false).
2146
+ if (!Grow(state)) {
2147
+ return false;
2148
+ }
2149
+ }
2150
+ // Success (didn't need to grow, or did successfully)
2151
+ return true;
2152
+ }
2153
+
2154
+ bool AutoHyperClockTable::Grow(InsertState& state) {
2155
+ size_t used_length = LengthInfoToUsedLength(state.saved_length_info);
2156
+
2157
+ // Try to take ownership of a grow slot as the first thread to set its
2158
+ // head_next_with_shift to non-zero, specifically a valid empty chain
2159
+ // in case that is to be the final value.
2160
+ // (We don't need to be super efficient here.)
2161
+ size_t grow_home = used_length;
2162
+ int old_shift;
2163
+ for (;; ++grow_home) {
2164
+ if (grow_home >= array_.Count()) {
2165
+ // Can't grow any more.
2166
+ // (Tested by unit test ClockCacheTest/Limits)
2167
+ return false;
2168
+ }
2169
+
2170
+ old_shift = FloorLog2(grow_home);
2171
+ assert(old_shift >= 1);
2172
+
2173
+ uint64_t empty_head = MakeNextWithShiftEnd(grow_home, old_shift + 1);
2174
+ uint64_t expected_zero = HandleImpl::kUnusedMarker;
2175
+ bool own = array_[grow_home].head_next_with_shift.compare_exchange_strong(
2176
+ expected_zero, empty_head, std::memory_order_acq_rel);
2177
+ if (own) {
2178
+ assert(array_[grow_home].meta.load(std::memory_order_acquire) == 0);
2179
+ break;
2180
+ } else {
2181
+ // Taken by another thread. Try next slot.
2182
+ assert(expected_zero != 0);
2183
+ }
2184
+ }
2185
+ #ifdef COERCE_CONTEXT_SWITCH
2186
+ // This is useful in reproducing concurrency issues in Grow()
2187
+ while (Random::GetTLSInstance()->OneIn(2)) {
2188
+ std::this_thread::yield();
2189
+ }
2190
+ #endif
2191
+ // Basically, to implement https://en.wikipedia.org/wiki/Linear_hashing
2192
+ // entries that belong in a new chain starting at grow_home will be
2193
+ // split off from the chain starting at old_home, which is computed here.
2194
+ size_t old_home = BottomNBits(grow_home, old_shift);
2195
+ assert(old_home + (size_t{1} << old_shift) == grow_home);
2196
+
2197
+ // Wait here to ensure any Grow operations that would directly feed into
2198
+ // this one are finished, though the full waiting actually completes in
2199
+ // acquiring the rewrite lock for old_home in SplitForGrow.
2200
+ size_t old_old_home = BottomNBits(grow_home, old_shift - 1);
2201
+ for (;;) {
2202
+ uint64_t old_old_head = array_[old_old_home].head_next_with_shift.load(
2203
+ std::memory_order_acquire);
2204
+ if (GetShiftFromNextWithShift(old_old_head) >= old_shift) {
2205
+ if ((old_old_head & HandleImpl::kNextEndFlags) !=
2206
+ HandleImpl::kHeadLocked) {
2207
+ break;
2208
+ }
2209
+ }
2210
+ // NOTE: one of the few yield-wait loops, which is rare enough in practice
2211
+ // for its performance to be insignificant.
2212
+ yield_count_.fetch_add(1, std::memory_order_relaxed);
2213
+ std::this_thread::yield();
2214
+ }
2215
+
2216
+ // Do the dirty work of splitting the chain, including updating heads and
2217
+ // chain nexts for new shift amounts.
2218
+ SplitForGrow(grow_home, old_home, old_shift);
2219
+
2220
+ // length_info_ can be updated any time after the new shift amount is
2221
+ // published to both heads, potentially before the end of SplitForGrow.
2222
+ // But we also can't update length_info_ until the previous Grow operation
2223
+ // (with grow_home := this grow_home - 1) has published the new shift amount
2224
+ // to both of its heads. However, we don't want to artificially wait here
2225
+ // on that Grow that is otherwise irrelevant.
2226
+ //
2227
+ // We could have each Grow operation advance length_info_ here as far as it
2228
+ // can without waiting, by checking for updated shift on the corresponding
2229
+ // old home and also stopping at an empty head value for possible grow_home.
2230
+ // However, this could increase CPU cache line sharing and in 1/64 cases
2231
+ // bring in an extra page from our mmap.
2232
+ //
2233
+ // Instead, part of the strategy is delegated to DoInsert():
2234
+ // * Here we try to bring length_info_ up to date with this grow_home as
2235
+ // much as we can without waiting. It will fall short if a previous Grow
2236
+ // is still between reserving the grow slot and making the first big step
2237
+ // to publish the new shift amount.
2238
+ // * To avoid length_info_ being perpetually out-of-date (for a small number
2239
+ // of heads) after our last Grow, we do the same when Insert has to "fall
2240
+ // forward" due to length_info_ being out-of-date.
2241
+ CatchUpLengthInfoNoWait(grow_home);
2242
+
2243
+ // See usage in DoInsert()
2244
+ state.likely_empty_slot = grow_home;
2245
+
2246
+ // Success
2247
+ return true;
2248
+ }
2249
+
2250
+ // See call in Grow()
2251
+ void AutoHyperClockTable::CatchUpLengthInfoNoWait(
2252
+ size_t known_usable_grow_home) {
2253
+ uint64_t current_length_info = length_info_.load(std::memory_order_acquire);
2254
+ size_t published_usable_size = LengthInfoToUsedLength(current_length_info);
2255
+ while (published_usable_size <= known_usable_grow_home) {
2256
+ // For when published_usable_size was grow_home
2257
+ size_t next_usable_size = published_usable_size + 1;
2258
+ uint64_t next_length_info = UsedLengthToLengthInfo(next_usable_size);
2259
+
2260
+ // known_usable_grow_home is known to be ready for Lookup/Insert with
2261
+ // the new shift amount, but between that and published usable size, we
2262
+ // need to check.
2263
+ if (published_usable_size < known_usable_grow_home) {
2264
+ int old_shift = FloorLog2(next_usable_size - 1);
2265
+ size_t old_home = BottomNBits(published_usable_size, old_shift);
2266
+ int shift =
2267
+ GetShiftFromNextWithShift(array_[old_home].head_next_with_shift.load(
2268
+ std::memory_order_acquire));
2269
+ if (shift <= old_shift) {
2270
+ // Not ready
2271
+ break;
2272
+ }
2273
+ }
2274
+ // CAS update length_info_. This only moves in one direction, so if CAS
2275
+ // fails, someone else made progress like we are trying, and we can just
2276
+ // pick up the new value and keep going as appropriate.
2277
+ if (length_info_.compare_exchange_strong(
2278
+ current_length_info, next_length_info, std::memory_order_acq_rel)) {
2279
+ current_length_info = next_length_info;
2280
+ // Update usage_ if metadata charge policy calls for it
2281
+ if (metadata_charge_policy_ ==
2282
+ CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
2283
+ // NOTE: ignoring page boundaries for simplicity
2284
+ usage_.fetch_add(sizeof(HandleImpl), std::memory_order_relaxed);
2285
+ }
2286
+ }
2287
+ published_usable_size = LengthInfoToUsedLength(current_length_info);
2288
+ }
2289
+
2290
+ // After updating lengh_info_ we can update occupancy_limit_,
2291
+ // allowing for later operations to update it before us.
2292
+ // Note: there is no std::atomic max operation, so we have to use a CAS loop
2293
+ size_t old_occupancy_limit = occupancy_limit_.load(std::memory_order_acquire);
2294
+ size_t new_occupancy_limit = CalcOccupancyLimit(published_usable_size);
2295
+ while (old_occupancy_limit < new_occupancy_limit) {
2296
+ if (occupancy_limit_.compare_exchange_weak(old_occupancy_limit,
2297
+ new_occupancy_limit,
2298
+ std::memory_order_acq_rel)) {
2299
+ break;
2300
+ }
2301
+ }
2302
+ }
2303
+
2304
+ void AutoHyperClockTable::SplitForGrow(size_t grow_home, size_t old_home,
2305
+ int old_shift) {
2306
+ int new_shift = old_shift + 1;
2307
+ HandleImpl* const arr = array_.Get();
2308
+
2309
+ // We implement a somewhat complicated splitting algorithm to ensure that
2310
+ // entries are always wait-free visible to Lookup, without Lookup needing
2311
+ // to double-check length_info_ to ensure every potentially relevant
2312
+ // existing entry is seen. This works step-by-step, carefully sharing
2313
+ // unmigrated parts of the chain between the source chain and the new
2314
+ // destination chain. This means that Lookup might see a partially migrated
2315
+ // chain so has to take that into consideration when checking that it hasn't
2316
+ // "jumped off" its intended chain (due to a parallel modification to an
2317
+ // "under (de)construction" entry that was found on the chain but has
2318
+ // been reassigned).
2319
+ //
2320
+ // We use a "rewrite lock" on the source and desination chains to exclude
2321
+ // removals from those, and we have a prior waiting step that ensures any Grow
2322
+ // operations feeding into this one have completed. But this process does have
2323
+ // to gracefully handle concurrent insertions to the head of the source chain,
2324
+ // and once marked ready, the destination chain.
2325
+ //
2326
+ // With those considerations, the migration starts with one "big step,"
2327
+ // potentially with retries to deal with insertions in parallel. Part of the
2328
+ // big step is to mark the two chain heads as updated with the new shift
2329
+ // amount, which redirects Lookups to the appropriate new chain.
2330
+ //
2331
+ // After that big step that updates the heads, the rewrite lock makes it
2332
+ // relatively easy to deal with the rest of the migration. Big
2333
+ // simplifications come from being able to read the hashed_key of each
2334
+ // entry on the chain without needing to hold a read reference, and
2335
+ // from never "jumping our to another chain." Concurrent insertions only
2336
+ // happen at the chain head, which is outside of what is left to migrate.
2337
+ //
2338
+ // A series of smaller steps finishes splitting apart the existing chain into
2339
+ // two distinct chains, followed by some steps to fully commit the result.
2340
+ //
2341
+ // Except for trivial cases in which all entries (or remaining entries)
2342
+ // on the input chain go to one output chain, there is an important invariant
2343
+ // after each step of migration, including after the initial "big step":
2344
+ // For each output chain, the "zero chain" (new hash bit is zero) and the
2345
+ // "one chain" (new hash bit is one) we have a "frontier" entry marking the
2346
+ // boundary between what has been migrated and what has not. One of the
2347
+ // frontiers is along the old chain after the other, and all entries between
2348
+ // them are for the same target chain as the earlier frontier. Thus, the
2349
+ // chains share linked list tails starting at the latter frontier. All
2350
+ // pointers from the new head locations to the frontier entries are marked
2351
+ // with the new shift amount, while all pointers after the frontiers use the
2352
+ // old shift amount.
2353
+ //
2354
+ // And after each step there is a strengthening step to reach a stronger
2355
+ // invariant: the frontier earlier in the original chain is advanced to be
2356
+ // immediately before the other frontier.
2357
+ //
2358
+ // Consider this original input chain,
2359
+ //
2360
+ // OldHome -Old-> A0 -Old-> B0 -Old-> A1 -Old-> C0 -Old-> OldHome(End)
2361
+ // GrowHome (empty)
2362
+ //
2363
+ // == BIG STEP ==
2364
+ // The initial big step finds the first entry that will be on the each
2365
+ // output chain (in this case A0 and A1). We use brackets ([]) to mark them
2366
+ // as our prospective frontiers.
2367
+ //
2368
+ // OldHome -Old-> [A0] -Old-> B0 -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2369
+ // GrowHome (empty)
2370
+ //
2371
+ // Next we speculatively update grow_home head to point to the first entry for
2372
+ // the one chain. This will not be used by Lookup until the head at old_home
2373
+ // uses the new shift amount.
2374
+ //
2375
+ // OldHome -Old-> [A0] -Old-> B0 -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2376
+ // GrowHome --------------New------------/
2377
+ //
2378
+ // Observe that if Lookup were to use the new head at GrowHome, it would be
2379
+ // able to find all relevant entries. Finishing the initial big step
2380
+ // requires a CAS (compare_exchange) of the OldHome head because there
2381
+ // might have been parallel insertions there, in which case we roll back
2382
+ // and try again. (We might need to point GrowHome head differently.)
2383
+ //
2384
+ // OldHome -New-> [A0] -Old-> B0 -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2385
+ // GrowHome --------------New------------/
2386
+ //
2387
+ // Upgrading the OldHome head pointer with the new shift amount, with a
2388
+ // compare_exchange, completes the initial big step, with [A0] as zero
2389
+ // chain frontier and [A1] as one chain frontier. Links before the frontiers
2390
+ // use the new shift amount and links after use the old shift amount.
2391
+ // == END BIG STEP==
2392
+ // == STRENGTHENING ==
2393
+ // Zero chain frontier is advanced to [B0] (immediately before other
2394
+ // frontier) by updating pointers with new shift amounts.
2395
+ //
2396
+ // OldHome -New-> A0 -New-> [B0] -Old-> [A1] -Old-> C0 -Old-> OldHome(End)
2397
+ // GrowHome -------------New-----------/
2398
+ //
2399
+ // == END STRENGTHENING ==
2400
+ // == SMALL STEP #1 ==
2401
+ // From the strong invariant state, we need to find the next entry for
2402
+ // the new chain with the earlier frontier. In this case, we need to find
2403
+ // the next entry for the zero chain that comes after [B0], which in this
2404
+ // case is C0. This will be our next zero chain frontier, at least under
2405
+ // the weak invariant. To get there, we simply update the link between
2406
+ // the current two frontiers to skip over the entries irreleveant to the
2407
+ // ealier frontier chain. In this case, the zero chain skips over A1. As a
2408
+ // result, he other chain is now the "earlier."
2409
+ //
2410
+ // OldHome -New-> A0 -New-> B0 -New-> [C0] -Old-> OldHome(End)
2411
+ // GrowHome -New-> [A1] ------Old-----/
2412
+ //
2413
+ // == END SMALL STEP #1 ==
2414
+ //
2415
+ // Repeating the cycle and end handling is not as interesting.
2416
+
2417
+ // Acquire rewrite lock on zero chain (if it's non-empty)
2418
+ ChainRewriteLock zero_head_lock(&arr[old_home], yield_count_);
2419
+ // Create an RAII wrapper for one chain rewrite lock, for once it becomes
2420
+ // non-empty. This head is unused by Lookup and DoInsert until the zero
2421
+ // head is updated with new shift amount.
2422
+ ChainRewriteLock one_head_lock(&arr[grow_home], yield_count_,
2423
+ /*already_locked_or_end=*/true);
2424
+ assert(one_head_lock.IsEnd());
2425
+
2426
+ // old_home will also the head of the new "zero chain" -- all entries in the
2427
+ // "from" chain whose next hash bit is 0. grow_home will be head of the new
2428
+ // "one chain".
2429
+
2430
+ // For these, SIZE_MAX is like nullptr (unknown)
2431
+ size_t zero_chain_frontier = SIZE_MAX;
2432
+ size_t one_chain_frontier = SIZE_MAX;
2433
+ size_t cur = SIZE_MAX;
2434
+
2435
+ // Set to 0 (zero chain frontier earlier), 1 (one chain), or -1 (unknown)
2436
+ int chain_frontier_first = -1;
2437
+
2438
+ // Might need to retry initial update of heads
2439
+ for (int i = 0;; ++i) {
2440
+ CHECK_TOO_MANY_ITERATIONS(i);
2441
+ assert(zero_chain_frontier == SIZE_MAX);
2442
+ assert(one_chain_frontier == SIZE_MAX);
2443
+ assert(cur == SIZE_MAX);
2444
+ assert(chain_frontier_first == -1);
2445
+
2446
+ uint64_t next_with_shift = zero_head_lock.GetNewHead();
2447
+
2448
+ // Find a single representative for each target chain, or scan the whole
2449
+ // chain if some target chain has no representative.
2450
+ for (;; ++i) {
2451
+ CHECK_TOO_MANY_ITERATIONS(i);
2452
+
2453
+ // Loop invariants
2454
+ assert((chain_frontier_first < 0) == (zero_chain_frontier == SIZE_MAX &&
2455
+ one_chain_frontier == SIZE_MAX));
2456
+ assert((cur == SIZE_MAX) == (zero_chain_frontier == SIZE_MAX &&
2457
+ one_chain_frontier == SIZE_MAX));
2458
+
2459
+ assert(GetShiftFromNextWithShift(next_with_shift) == old_shift);
2460
+
2461
+ // Check for end of original chain
2462
+ if (HandleImpl::IsEnd(next_with_shift)) {
2463
+ cur = SIZE_MAX;
2464
+ break;
2465
+ }
2466
+
2467
+ // next_with_shift is not End
2468
+ cur = GetNextFromNextWithShift(next_with_shift);
2469
+
2470
+ if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
2471
+ // Entry for zero chain
2472
+ if (zero_chain_frontier == SIZE_MAX) {
2473
+ zero_chain_frontier = cur;
2474
+ if (one_chain_frontier != SIZE_MAX) {
2475
+ // Ready to update heads
2476
+ break;
2477
+ }
2478
+ // Nothing yet for one chain
2479
+ chain_frontier_first = 0;
2480
+ }
2481
+ } else {
2482
+ assert(BottomNBits(arr[cur].hashed_key[1], new_shift) == grow_home);
2483
+ // Entry for one chain
2484
+ if (one_chain_frontier == SIZE_MAX) {
2485
+ one_chain_frontier = cur;
2486
+ if (zero_chain_frontier != SIZE_MAX) {
2487
+ // Ready to update heads
2488
+ break;
2489
+ }
2490
+ // Nothing yet for zero chain
2491
+ chain_frontier_first = 1;
2492
+ }
2493
+ }
2494
+
2495
+ next_with_shift =
2496
+ arr[cur].chain_next_with_shift.load(std::memory_order_acquire);
2497
+ }
2498
+
2499
+ // Try to update heads for initial migration info
2500
+ // We only reached the end of the migrate-from chain already if one of the
2501
+ // target chains will be empty.
2502
+ assert((cur == SIZE_MAX) ==
2503
+ (zero_chain_frontier == SIZE_MAX || one_chain_frontier == SIZE_MAX));
2504
+ assert((chain_frontier_first < 0) ==
2505
+ (zero_chain_frontier == SIZE_MAX && one_chain_frontier == SIZE_MAX));
2506
+
2507
+ // Always update one chain's head first (safe).
2508
+ one_head_lock.SimpleUpdate(
2509
+ one_chain_frontier != SIZE_MAX
2510
+ ? MakeNextWithShift(one_chain_frontier, new_shift)
2511
+ : MakeNextWithShiftEnd(grow_home, new_shift));
2512
+
2513
+ // Make sure length_info_ hasn't been updated too early, as we're about
2514
+ // to make the change that makes it safe to update (e.g. in DoInsert())
2515
+ assert(LengthInfoToUsedLength(
2516
+ length_info_.load(std::memory_order_acquire)) <= grow_home);
2517
+
2518
+ // Try to set zero's head.
2519
+ if (zero_head_lock.CasUpdate(
2520
+ zero_chain_frontier != SIZE_MAX
2521
+ ? MakeNextWithShift(zero_chain_frontier, new_shift)
2522
+ : MakeNextWithShiftEnd(old_home, new_shift),
2523
+ yield_count_)) {
2524
+ // Both heads successfully updated to new shift
2525
+ break;
2526
+ } else {
2527
+ // Concurrent insertion. This should not happen too many times.
2528
+ CHECK_TOO_MANY_ITERATIONS(i);
2529
+ // The easiest solution is to restart.
2530
+ zero_chain_frontier = SIZE_MAX;
2531
+ one_chain_frontier = SIZE_MAX;
2532
+ cur = SIZE_MAX;
2533
+ chain_frontier_first = -1;
2534
+ continue;
2535
+ }
2536
+ }
2537
+
2538
+ // Except for trivial cases, we have something like
2539
+ // AHome -New-> [A0] -Old-> [B0] -Old-> [C0] \ |
2540
+ // BHome --------------------New------------> [A1] -Old-> ...
2541
+ // And we need to upgrade as much as we can on the "first" chain
2542
+ // (the one eventually pointing to the other's frontier). This will
2543
+ // also finish off any case in which one of the target chains will be empty.
2544
+ if (chain_frontier_first >= 0) {
2545
+ size_t& first_frontier = chain_frontier_first == 0
2546
+ ? /*&*/ zero_chain_frontier
2547
+ : /*&*/ one_chain_frontier;
2548
+ size_t& other_frontier = chain_frontier_first != 0
2549
+ ? /*&*/ zero_chain_frontier
2550
+ : /*&*/ one_chain_frontier;
2551
+ uint64_t stop_before_or_new_tail =
2552
+ other_frontier != SIZE_MAX
2553
+ ? /*stop before*/ MakeNextWithShift(other_frontier, old_shift)
2554
+ : /*new tail*/ MakeNextWithShiftEnd(
2555
+ chain_frontier_first == 0 ? old_home : grow_home, new_shift);
2556
+ UpgradeShiftsOnRange(arr, first_frontier, stop_before_or_new_tail,
2557
+ old_shift, new_shift);
2558
+ }
2559
+
2560
+ if (zero_chain_frontier == SIZE_MAX) {
2561
+ // Already finished migrating
2562
+ assert(one_chain_frontier == SIZE_MAX);
2563
+ assert(cur == SIZE_MAX);
2564
+ } else {
2565
+ // Still need to migrate between two target chains
2566
+ for (int i = 0;; ++i) {
2567
+ CHECK_TOO_MANY_ITERATIONS(i);
2568
+ // Overall loop invariants
2569
+ assert(zero_chain_frontier != SIZE_MAX);
2570
+ assert(one_chain_frontier != SIZE_MAX);
2571
+ assert(cur != SIZE_MAX);
2572
+ assert(chain_frontier_first >= 0);
2573
+ size_t& first_frontier = chain_frontier_first == 0
2574
+ ? /*&*/ zero_chain_frontier
2575
+ : /*&*/ one_chain_frontier;
2576
+ size_t& other_frontier = chain_frontier_first != 0
2577
+ ? /*&*/ zero_chain_frontier
2578
+ : /*&*/ one_chain_frontier;
2579
+ assert(cur != first_frontier);
2580
+ assert(GetNextFromNextWithShift(
2581
+ arr[first_frontier].chain_next_with_shift.load(
2582
+ std::memory_order_acquire)) == other_frontier);
2583
+
2584
+ uint64_t next_with_shift =
2585
+ arr[cur].chain_next_with_shift.load(std::memory_order_acquire);
2586
+
2587
+ // Check for end of original chain
2588
+ if (HandleImpl::IsEnd(next_with_shift)) {
2589
+ // Can set upgraded tail on first chain
2590
+ uint64_t first_new_tail = MakeNextWithShiftEnd(
2591
+ chain_frontier_first == 0 ? old_home : grow_home, new_shift);
2592
+ arr[first_frontier].chain_next_with_shift.store(
2593
+ first_new_tail, std::memory_order_release);
2594
+ // And upgrade remainder of other chain
2595
+ uint64_t other_new_tail = MakeNextWithShiftEnd(
2596
+ chain_frontier_first != 0 ? old_home : grow_home, new_shift);
2597
+ UpgradeShiftsOnRange(arr, other_frontier, other_new_tail, old_shift,
2598
+ new_shift);
2599
+ assert(other_frontier == SIZE_MAX); // Finished
2600
+ break;
2601
+ }
2602
+
2603
+ // next_with_shift is not End
2604
+ cur = GetNextFromNextWithShift(next_with_shift);
2605
+
2606
+ int target_chain;
2607
+ if (BottomNBits(arr[cur].hashed_key[1], new_shift) == old_home) {
2608
+ // Entry for zero chain
2609
+ target_chain = 0;
2610
+ } else {
2611
+ assert(BottomNBits(arr[cur].hashed_key[1], new_shift) == grow_home);
2612
+ // Entry for one chain
2613
+ target_chain = 1;
2614
+ }
2615
+ if (target_chain == chain_frontier_first) {
2616
+ // Found next entry to skip to on the first chain
2617
+ uint64_t skip_to = MakeNextWithShift(cur, new_shift);
2618
+ arr[first_frontier].chain_next_with_shift.store(
2619
+ skip_to, std::memory_order_release);
2620
+ first_frontier = cur;
2621
+ // Upgrade other chain up to entry before that one
2622
+ UpgradeShiftsOnRange(arr, other_frontier, next_with_shift, old_shift,
2623
+ new_shift);
2624
+ // Swap which is marked as first
2625
+ chain_frontier_first = 1 - chain_frontier_first;
2626
+ } else {
2627
+ // Nothing to do yet, as we need to keep old generation pointers in
2628
+ // place for lookups
2629
+ }
2630
+ }
2631
+ }
2632
+ }
2633
+
2634
+ // Variant of PurgeImplLocked: Removes all "under (de) construction" entries
2635
+ // from a chain where already holding a rewrite lock
2636
+ using PurgeLockedOpData = void;
2637
+ // Variant of PurgeImplLocked: Clock-updates all entries in a chain, in
2638
+ // addition to functionality of PurgeLocked, where already holding a rewrite
2639
+ // lock. (Caller finalizes eviction on entries added to the autovector, in part
2640
+ // so that we don't hold the rewrite lock while doing potentially expensive
2641
+ // callback and allocator free.)
2642
+ using ClockUpdateChainLockedOpData =
2643
+ autovector<AutoHyperClockTable::HandleImpl*>;
2644
+
2645
+ template <class OpData>
2646
+ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
2647
+ ChainRewriteLock& rewrite_lock,
2648
+ size_t home) {
2649
+ constexpr bool kIsPurge = std::is_same_v<OpData, PurgeLockedOpData>;
2650
+ constexpr bool kIsClockUpdateChain =
2651
+ std::is_same_v<OpData, ClockUpdateChainLockedOpData>;
2652
+
2653
+ // Exactly one op specified
2654
+ static_assert(kIsPurge + kIsClockUpdateChain == 1);
2655
+
2656
+ HandleImpl* const arr = array_.Get();
2657
+
2658
+ uint64_t next_with_shift = rewrite_lock.GetNewHead();
2659
+ assert(!HandleImpl::IsEnd(next_with_shift));
2660
+ int home_shift = GetShiftFromNextWithShift(next_with_shift);
2661
+ (void)home;
2662
+ (void)home_shift;
2663
+ size_t next = GetNextFromNextWithShift(next_with_shift);
2664
+ assert(next < array_.Count());
2665
+ HandleImpl* h = &arr[next];
2666
+ HandleImpl* prev_to_keep = nullptr;
2667
+ #ifndef NDEBUG
2668
+ uint64_t prev_to_keep_next_with_shift = 0;
2669
+ #endif
2670
+ // Whether there are entries between h and prev_to_keep that should be
2671
+ // purged from the chain.
2672
+ bool pending_purge = false;
2673
+
2674
+ // Walk the chain, and stitch together any entries that are still
2675
+ // "shareable," possibly after clock update. prev_to_keep tells us where
2676
+ // the last "stitch back to" location is (nullptr => head).
2677
+ for (size_t i = 0;; ++i) {
2678
+ CHECK_TOO_MANY_ITERATIONS(i);
2679
+
2680
+ bool purgeable = false;
2681
+ // In last iteration, h will be nullptr, to stitch together the tail of
2682
+ // the chain.
2683
+ if (h) {
2684
+ // NOTE: holding a rewrite lock on the chain prevents any "under
2685
+ // (de)construction" entries in the chain from being marked empty, which
2686
+ // allows us to access the hashed_keys without holding a read ref.
2687
+ assert(home == BottomNBits(h->hashed_key[1], home_shift));
2688
+ if constexpr (kIsClockUpdateChain) {
2689
+ // Clock update and/or check for purgeable (under (de)construction)
2690
+ if (ClockUpdate(*h, &purgeable)) {
2691
+ // Remember for finishing eviction
2692
+ op_data->push_back(h);
2693
+ // Entries for eviction become purgeable
2694
+ purgeable = true;
2695
+ assert((h->meta.load(std::memory_order_acquire) >>
2696
+ ClockHandle::kStateShift) == ClockHandle::kStateConstruction);
2697
+ }
2698
+ } else {
2699
+ (void)op_data;
2700
+ purgeable = ((h->meta.load(std::memory_order_acquire) >>
2701
+ ClockHandle::kStateShift) &
2702
+ ClockHandle::kStateShareableBit) == 0;
2703
+ }
2704
+ }
2705
+
2706
+ if (purgeable) {
2707
+ assert((h->meta.load(std::memory_order_acquire) >>
2708
+ ClockHandle::kStateShift) == ClockHandle::kStateConstruction);
2709
+ pending_purge = true;
2710
+ } else if (pending_purge) {
2711
+ if (prev_to_keep) {
2712
+ // Update chain next to skip purgeable entries
2713
+ assert(prev_to_keep->chain_next_with_shift.load(
2714
+ std::memory_order_acquire) == prev_to_keep_next_with_shift);
2715
+ prev_to_keep->chain_next_with_shift.store(next_with_shift,
2716
+ std::memory_order_release);
2717
+ } else if (rewrite_lock.CasUpdate(next_with_shift, yield_count_)) {
2718
+ // Managed to update head without any parallel insertions
2719
+ } else {
2720
+ // Parallel insertion must have interfered. Need to do a purge
2721
+ // from updated head to here. Since we have no prev_to_keep, there's
2722
+ // no risk of duplicate clock updates to entries. Any entries already
2723
+ // updated must have been evicted (purgeable) and it's OK to clock
2724
+ // update any new entries just inserted in parallel.
2725
+ // Can simply restart (GetNewHead() already updated from CAS failure).
2726
+ next_with_shift = rewrite_lock.GetNewHead();
2727
+ assert(!HandleImpl::IsEnd(next_with_shift));
2728
+ next = GetNextFromNextWithShift(next_with_shift);
2729
+ assert(next < array_.Count());
2730
+ h = &arr[next];
2731
+ pending_purge = false;
2732
+ assert(prev_to_keep == nullptr);
2733
+ assert(GetShiftFromNextWithShift(next_with_shift) == home_shift);
2734
+ continue;
2735
+ }
2736
+ pending_purge = false;
2737
+ prev_to_keep = h;
2738
+ } else {
2739
+ prev_to_keep = h;
2740
+ }
2741
+
2742
+ if (h == nullptr) {
2743
+ // Reached end of the chain
2744
+ return;
2745
+ }
2746
+
2747
+ // Read chain pointer
2748
+ next_with_shift = h->chain_next_with_shift.load(std::memory_order_acquire);
2749
+ #ifndef NDEBUG
2750
+ if (prev_to_keep == h) {
2751
+ prev_to_keep_next_with_shift = next_with_shift;
2752
+ }
2753
+ #endif
2754
+
2755
+ assert(GetShiftFromNextWithShift(next_with_shift) == home_shift);
2756
+
2757
+ // Check for end marker
2758
+ if (HandleImpl::IsEnd(next_with_shift)) {
2759
+ h = nullptr;
2760
+ } else {
2761
+ next = GetNextFromNextWithShift(next_with_shift);
2762
+ assert(next < array_.Count());
2763
+ h = &arr[next];
2764
+ assert(h != prev_to_keep);
2765
+ }
2766
+ }
2767
+ }
2768
+
2769
+ // Variant of PurgeImpl: Removes all "under (de) construction" entries in a
2770
+ // chain, such that any entry with the given key must have been purged.
2771
+ using PurgeOpData = const UniqueId64x2;
2772
+ // Variant of PurgeImpl: Clock-updates all entries in a chain, in addition to
2773
+ // purging as appropriate. (Caller finalizes eviction on entries added to the
2774
+ // autovector, in part so that we don't hold the rewrite lock while doing
2775
+ // potentially expensive callback and allocator free.)
2776
+ using ClockUpdateChainOpData = ClockUpdateChainLockedOpData;
2777
+
2778
+ template <class OpData>
2779
+ void AutoHyperClockTable::PurgeImpl(OpData* op_data, size_t home) {
2780
+ // Early efforts to make AutoHCC fully wait-free ran into too many problems
2781
+ // that needed obscure and potentially inefficient work-arounds to have a
2782
+ // chance at working.
2783
+ //
2784
+ // The implementation settled on "essentially wait-free" which can be
2785
+ // achieved by locking at the level of each probing chain and only for
2786
+ // operations that might remove entries from the chain. Because parallel
2787
+ // clock updates and Grow operations are ordered, contention is very rare.
2788
+ // However, parallel insertions at any chain head have to be accommodated
2789
+ // to keep them wait-free.
2790
+ //
2791
+ // This function implements Purge and ClockUpdateChain functions (see above
2792
+ // OpData type definitions) as part of higher-level operations. This function
2793
+ // ensures the correct chain is (eventually) covered and handles rewrite
2794
+ // locking the chain. PurgeImplLocked has lower level details.
2795
+ //
2796
+ // In general, these operations and Grow are kept simpler by allowing eager
2797
+ // purging of under (de-)construction entries. For example, an Erase
2798
+ // operation might find that another thread has purged the entry from the
2799
+ // chain by the time its own purge operation acquires the rewrite lock and
2800
+ // proceeds. This is OK, and potentially reduces the number of lock/unlock
2801
+ // cycles because empty chains are not rewrite-lockable.
2802
+
2803
+ constexpr bool kIsPurge = std::is_same_v<OpData, PurgeOpData>;
2804
+ constexpr bool kIsClockUpdateChain =
2805
+ std::is_same_v<OpData, ClockUpdateChainOpData>;
2806
+
2807
+ // Exactly one op specified
2808
+ static_assert(kIsPurge + kIsClockUpdateChain == 1);
2809
+
2810
+ int home_shift = 0;
2811
+ if constexpr (kIsPurge) {
2812
+ // Purge callers leave home unspecified, to be determined from key
2813
+ assert(home == SIZE_MAX);
2814
+ GetHomeIndexAndShift(length_info_.load(std::memory_order_acquire),
2815
+ (*op_data)[1], &home, &home_shift);
2816
+ assert(home_shift > 0);
2817
+ } else {
2818
+ // Evict callers must specify home
2819
+ assert(home < SIZE_MAX);
2820
+ }
2821
+
2822
+ HandleImpl* const arr = array_.Get();
2823
+
2824
+ // Acquire the RAII rewrite lock (if not an empty chain)
2825
+ ChainRewriteLock rewrite_lock(&arr[home], yield_count_);
2826
+
2827
+ int shift;
2828
+ for (;;) {
2829
+ shift = GetShiftFromNextWithShift(rewrite_lock.GetNewHead());
2830
+
2831
+ if constexpr (kIsPurge) {
2832
+ if (shift > home_shift) {
2833
+ // At head. Thus, we know the newer shift applies to us.
2834
+ // Newer shift might not yet be reflected in length_info_ (an atomicity
2835
+ // gap in Grow), so operate as if it is. Note that other insertions
2836
+ // could happen using this shift before length_info_ is updated, and
2837
+ // it's possible (though unlikely) that multiple generations of Grow
2838
+ // have occurred. If shift is more than one generation ahead of
2839
+ // home_shift, it's possible that not all descendent homes have
2840
+ // reached the `shift` generation. Thus, we need to advance only one
2841
+ // shift at a time looking for a home+head with a matching shift
2842
+ // amount.
2843
+ home_shift++;
2844
+ home = GetHomeIndex((*op_data)[1], home_shift);
2845
+ rewrite_lock.Reset(&arr[home], yield_count_);
2846
+ continue;
2847
+ } else {
2848
+ assert(shift == home_shift);
2849
+ }
2850
+ } else {
2851
+ assert(home_shift == 0);
2852
+ home_shift = shift;
2853
+ }
2854
+ break;
2855
+ }
2856
+
2857
+ // If the chain is empty, nothing to do
2858
+ if (!rewrite_lock.IsEnd()) {
2859
+ if constexpr (kIsPurge) {
2860
+ PurgeLockedOpData* locked_op_data{};
2861
+ PurgeImplLocked(locked_op_data, rewrite_lock, home);
2862
+ } else {
2863
+ PurgeImplLocked(op_data, rewrite_lock, home);
2864
+ }
2865
+ }
2866
+ }
2867
+
2868
+ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
2869
+ const ClockHandleBasicData& proto, uint64_t initial_countdown,
2870
+ bool take_ref, InsertState& state) {
2871
+ size_t home;
2872
+ int orig_home_shift;
2873
+ GetHomeIndexAndShift(state.saved_length_info, proto.hashed_key[1], &home,
2874
+ &orig_home_shift);
2875
+ HandleImpl* const arr = array_.Get();
2876
+
2877
+ // We could go searching through the chain for any duplicate, but that's
2878
+ // not typically helpful, except for the REDUNDANT block cache stats.
2879
+ // (Inferior duplicates will age out with eviction.) However, we do skip
2880
+ // insertion if the home slot (or some other we happen to probe) already
2881
+ // has a match (already_matches below). This helps to keep better locality
2882
+ // when we can.
2883
+ //
2884
+ // And we can do that as part of searching for an available slot to
2885
+ // insert the new entry, because our preferred location and first slot
2886
+ // checked will be the home slot.
2887
+ //
2888
+ // As the table initially grows to size, few entries will be in the same
2889
+ // cache line as the chain head. However, churn in the cache relatively
2890
+ // quickly improves the proportion of entries sharing that cache line with
2891
+ // the chain head. Data:
2892
+ //
2893
+ // Initial population only: (cache_bench with -ops_per_thread=1)
2894
+ // Entries at home count: 29,202 (out of 129,170 entries in 94,411 chains)
2895
+ // Approximate average cache lines read to find an existing entry:
2896
+ // 129.2 / 94.4 [without the heads]
2897
+ // + (94.4 - 29.2) / 94.4 [the heads not included with entries]
2898
+ // = 2.06 cache lines
2899
+ //
2900
+ // After 10 million ops: (-threads=10 -ops_per_thread=100000)
2901
+ // Entries at home count: 67,556 (out of 129,359 entries in 94,756 chains)
2902
+ // That's a majority of entries and more than 2/3rds of chains.
2903
+ // Approximate average cache lines read to find an existing entry:
2904
+ // = 1.65 cache lines
2905
+
2906
+ size_t used_length = LengthInfoToUsedLength(state.saved_length_info);
2907
+ assert(home < used_length);
2908
+
2909
+ size_t idx = home;
2910
+ bool already_matches = false;
2911
+ bool already_matches_ignore = false;
2912
+ if (TryInsert(proto, arr[idx], initial_countdown, take_ref,
2913
+ &already_matches)) {
2914
+ assert(idx == home);
2915
+ } else if (already_matches) {
2916
+ return nullptr;
2917
+ // Here we try to populate newly-opened slots in the table, but not
2918
+ // when we can add something to its home slot. This makes the structure
2919
+ // more performant more quickly on (initial) growth. We ignore "already
2920
+ // matches" in this case because it is unlikely and difficult to
2921
+ // incorporate logic for here cleanly and efficiently.
2922
+ } else if (UNLIKELY(state.likely_empty_slot > 0) &&
2923
+ TryInsert(proto, arr[state.likely_empty_slot], initial_countdown,
2924
+ take_ref, &already_matches_ignore)) {
2925
+ idx = state.likely_empty_slot;
2926
+ } else {
2927
+ // We need to search for an available slot outside of the home.
2928
+ // Linear hashing provides nice resizing but does typically mean
2929
+ // that some heads (home locations) have (in expectation) twice as
2930
+ // many entries mapped to them as other heads. For example if the
2931
+ // usable length is 80, then heads 16-63 are (in expectation) twice
2932
+ // as loaded as heads 0-15 and 64-79, which are using another hash bit.
2933
+ //
2934
+ // This means that if we just use linear probing (by a small constant)
2935
+ // to find an available slot, part of the structure could easily fill up
2936
+ // and resort to linear time operations even when the overall load factor
2937
+ // is only modestly high, like 70%. Even though each slot has its own CPU
2938
+ // cache line, there appears to be a small locality benefit (e.g. TLB and
2939
+ // paging) to iterating one by one, as long as we don't afoul of the
2940
+ // linear hashing imbalance.
2941
+ //
2942
+ // In a traditional non-concurrent structure, we could keep a "free list"
2943
+ // to ensure immediate access to an available slot, but maintaining such
2944
+ // a structure could require more cross-thread coordination to ensure
2945
+ // all entries are eventually available to all threads.
2946
+ //
2947
+ // The way we solve this problem is to use unit-increment linear probing
2948
+ // with a small bound, and then fall back on big jumps to have a good
2949
+ // chance of finding a slot in an under-populated region quickly if that
2950
+ // doesn't work.
2951
+ size_t i = 0;
2952
+ constexpr size_t kMaxLinearProbe = 4;
2953
+ for (; i < kMaxLinearProbe; i++) {
2954
+ idx++;
2955
+ if (idx >= used_length) {
2956
+ idx -= used_length;
2957
+ }
2958
+ if (TryInsert(proto, arr[idx], initial_countdown, take_ref,
2959
+ &already_matches)) {
2960
+ break;
2961
+ }
2962
+ if (already_matches) {
2963
+ return nullptr;
2964
+ }
2965
+ }
2966
+ if (i == kMaxLinearProbe) {
2967
+ // Keep searching, but change to a search method that should quickly
2968
+ // find any under-populated region. Switching to an increment based
2969
+ // on the golden ratio helps with that, but we also inject some minor
2970
+ // variation (less than 2%, 1 in 2^6) to avoid clustering effects on
2971
+ // this larger increment (if it were a fixed value in steady state
2972
+ // operation). Here we are primarily using upper bits of hashed_key[1]
2973
+ // while home is based on lowest bits.
2974
+ uint64_t incr_ratio = 0x9E3779B185EBCA87U + (proto.hashed_key[1] >> 6);
2975
+ size_t incr = FastRange64(incr_ratio, used_length);
2976
+ assert(incr > 0);
2977
+ size_t start = idx;
2978
+ for (;; i++) {
2979
+ idx += incr;
2980
+ if (idx >= used_length) {
2981
+ // Wrap around (faster than %)
2982
+ idx -= used_length;
2983
+ }
2984
+ if (idx == start) {
2985
+ // We have just completed a cycle that might not have covered all
2986
+ // slots. (incr and used_length could have common factors.)
2987
+ // Increment for the next cycle, which eventually ensures complete
2988
+ // iteration over the set of slots before repeating.
2989
+ idx++;
2990
+ if (idx >= used_length) {
2991
+ idx -= used_length;
2992
+ }
2993
+ start++;
2994
+ if (start >= used_length) {
2995
+ start -= used_length;
2996
+ }
2997
+ if (i >= used_length) {
2998
+ used_length = LengthInfoToUsedLength(
2999
+ length_info_.load(std::memory_order_acquire));
3000
+ if (i >= used_length * 2) {
3001
+ // Cycling back should not happen unless there is enough random
3002
+ // churn in parallel that we happen to hit each slot at a time
3003
+ // that it's occupied, which is really only feasible for small
3004
+ // structures, though with linear probing to find empty slots,
3005
+ // "small" here might be larger than for double hashing.
3006
+ assert(used_length <= 256);
3007
+ // Fall back on standalone insert in case something goes awry to
3008
+ // cause this
3009
+ return nullptr;
3010
+ }
3011
+ }
3012
+ }
3013
+ if (TryInsert(proto, arr[idx], initial_countdown, take_ref,
3014
+ &already_matches)) {
3015
+ break;
3016
+ }
3017
+ if (already_matches) {
3018
+ return nullptr;
3019
+ }
3020
+ }
3021
+ }
3022
+ }
3023
+
3024
+ // Now insert into chain using head pointer
3025
+ uint64_t next_with_shift;
3026
+ int home_shift = orig_home_shift;
3027
+
3028
+ // Might need to retry
3029
+ for (int i = 0;; ++i) {
3030
+ CHECK_TOO_MANY_ITERATIONS(i);
3031
+ next_with_shift =
3032
+ arr[home].head_next_with_shift.load(std::memory_order_acquire);
3033
+ int shift = GetShiftFromNextWithShift(next_with_shift);
3034
+
3035
+ if (UNLIKELY(shift != home_shift)) {
3036
+ // NOTE: shift increases with table growth
3037
+ if (shift > home_shift) {
3038
+ // Must be grow in progress or completed since reading length_info.
3039
+ // Pull out one more hash bit. (See Lookup() for why we can't
3040
+ // safely jump to the shift that was read.)
3041
+ home_shift++;
3042
+ uint64_t hash_bit_mask = uint64_t{1} << (home_shift - 1);
3043
+ assert((home & hash_bit_mask) == 0);
3044
+ // BEGIN leftover updates to length_info_ for Grow()
3045
+ size_t grow_home = home + hash_bit_mask;
3046
+ assert(arr[grow_home].head_next_with_shift.load(
3047
+ std::memory_order_acquire) != HandleImpl::kUnusedMarker);
3048
+ CatchUpLengthInfoNoWait(grow_home);
3049
+ // END leftover updates to length_info_ for Grow()
3050
+ home += proto.hashed_key[1] & hash_bit_mask;
3051
+ continue;
3052
+ } else {
3053
+ // Should not happen because length_info_ is only updated after both
3054
+ // old and new home heads are marked with new shift
3055
+ assert(false);
3056
+ }
3057
+ }
3058
+
3059
+ // Values to update to
3060
+ uint64_t head_next_with_shift = MakeNextWithShift(idx, home_shift);
3061
+ uint64_t chain_next_with_shift = next_with_shift;
3062
+
3063
+ // Preserve the locked state in head, without propagating to chain next
3064
+ // where it is meaningless (and not allowed)
3065
+ if (UNLIKELY((next_with_shift & HandleImpl::kNextEndFlags) ==
3066
+ HandleImpl::kHeadLocked)) {
3067
+ head_next_with_shift |= HandleImpl::kHeadLocked;
3068
+ chain_next_with_shift &= ~HandleImpl::kHeadLocked;
3069
+ }
3070
+
3071
+ arr[idx].chain_next_with_shift.store(chain_next_with_shift,
3072
+ std::memory_order_release);
3073
+ if (arr[home].head_next_with_shift.compare_exchange_weak(
3074
+ next_with_shift, head_next_with_shift, std::memory_order_acq_rel)) {
3075
+ // Success
3076
+ return arr + idx;
3077
+ }
3078
+ }
3079
+ }
3080
+
3081
+ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
3082
+ const UniqueId64x2& hashed_key) {
3083
+ // Lookups are wait-free with low occurrence of retries, back-tracking,
3084
+ // and fallback. We do not have the benefit of holding a rewrite lock on
3085
+ // the chain so must be prepared for many kinds of mayhem, most notably
3086
+ // "falling off our chain" where a slot that Lookup has identified but
3087
+ // has not read-referenced is removed from one chain and inserted into
3088
+ // another. The full algorithm uses the following mitigation strategies to
3089
+ // ensure every relevant entry inserted before this Lookup, and not yet
3090
+ // evicted, is seen by Lookup, without excessive backtracking etc.:
3091
+ // * Keep a known good read ref in the chain for "island hopping." When
3092
+ // we observe that a concurrent write takes us off to another chain, we
3093
+ // only need to fall back to our last known good read ref (most recent
3094
+ // entry on the chain that is not "under construction," which is a transient
3095
+ // state). We don't want to compound the CPU toil of a long chain with
3096
+ // operations that might need to retry from scratch, with probability
3097
+ // in proportion to chain length.
3098
+ // * Only detect a chain is potentially incomplete because of a Grow in
3099
+ // progress by looking at shift in the next pointer tags (rather than
3100
+ // re-checking length_info_).
3101
+ // * SplitForGrow, Insert, and PurgeImplLocked ensure that there are no
3102
+ // transient states that might cause this full Lookup algorithm to skip over
3103
+ // live entries.
3104
+
3105
+ // Reading length_info_ is not strictly required for Lookup, if we were
3106
+ // to increment shift sizes until we see a shift size match on the
3107
+ // relevant head pointer. Thus, reading with relaxed memory order gives
3108
+ // us a safe and almost always up-to-date jump into finding the correct
3109
+ // home and head.
3110
+ size_t home;
3111
+ int home_shift;
3112
+ GetHomeIndexAndShift(length_info_.load(std::memory_order_relaxed),
3113
+ hashed_key[1], &home, &home_shift);
3114
+ assert(home_shift > 0);
3115
+
3116
+ // The full Lookup algorithm however is not great for hot path efficiency,
3117
+ // because of the extra careful tracking described above. Overwhelmingly,
3118
+ // we can find what we're looking for with a naive linked list traversal
3119
+ // of the chain. Even if we "fall off our chain" to another, we don't
3120
+ // violate memory safety. We just won't match the key we're looking for.
3121
+ // And we would eventually reach an end state, possibly even experiencing a
3122
+ // cycle as an entry is freed and reused during our traversal (though at
3123
+ // any point in time the structure doesn't have cycles).
3124
+ //
3125
+ // So for hot path efficiency, we start with a naive Lookup attempt, and
3126
+ // then fall back on full Lookup if we don't find the correct entry. To
3127
+ // cap how much we invest into the naive Lookup, we simply cap the traversal
3128
+ // length before falling back. Also, when we do fall back on full Lookup,
3129
+ // we aren't paying much penalty by starting over. Much or most of the cost
3130
+ // of Lookup is memory latency in following the chain pointers, and the
3131
+ // naive Lookup has warmed the CPU cache for these entries, using as tight
3132
+ // of a loop as possible.
3133
+
3134
+ HandleImpl* const arr = array_.Get();
3135
+ uint64_t next_with_shift = arr[home].head_next_with_shift;
3136
+ for (size_t i = 0; !HandleImpl::IsEnd(next_with_shift) && i < 10; ++i) {
3137
+ HandleImpl* h = &arr[GetNextFromNextWithShift(next_with_shift)];
3138
+ // Attempt cheap key match without acquiring a read ref. This could give a
3139
+ // false positive, which is re-checked after acquiring read ref, or false
3140
+ // negative, which is re-checked in the full Lookup. Also, this is a
3141
+ // technical UB data race according to TSAN, but we don't need to read
3142
+ // a "correct" value here for correct overall behavior.
3143
+ #ifdef __SANITIZE_THREAD__
3144
+ bool probably_equal = Random::GetTLSInstance()->OneIn(2);
3145
+ #else
3146
+ bool probably_equal = h->hashed_key == hashed_key;
3147
+ #endif
3148
+ if (probably_equal) {
3149
+ // Increment acquire counter for definitive check
3150
+ uint64_t old_meta = h->meta.fetch_add(ClockHandle::kAcquireIncrement,
3151
+ std::memory_order_acquire);
3152
+ // Check if it's a referencable (sharable) entry
3153
+ if (LIKELY(old_meta & (uint64_t{ClockHandle::kStateShareableBit}
3154
+ << ClockHandle::kStateShift))) {
3155
+ assert(GetRefcount(old_meta + ClockHandle::kAcquireIncrement) > 0);
3156
+ if (LIKELY(h->hashed_key == hashed_key) &&
3157
+ LIKELY(old_meta & (uint64_t{ClockHandle::kStateVisibleBit}
3158
+ << ClockHandle::kStateShift))) {
3159
+ return h;
3160
+ } else {
3161
+ Unref(*h);
3162
+ }
3163
+ } else {
3164
+ // For non-sharable states, incrementing the acquire counter has no
3165
+ // effect so we don't need to undo it. Furthermore, we cannot safely
3166
+ // undo it because we did not acquire a read reference to lock the entry
3167
+ // in a Shareable state.
3168
+ }
3169
+ }
3170
+
3171
+ next_with_shift = h->chain_next_with_shift.load(std::memory_order_relaxed);
3172
+ }
3173
+
3174
+ // If we get here, falling back on full Lookup algorithm.
3175
+ HandleImpl* h = nullptr;
3176
+ HandleImpl* read_ref_on_chain = nullptr;
3177
+
3178
+ for (size_t i = 0;; ++i) {
3179
+ CHECK_TOO_MANY_ITERATIONS(i);
3180
+ // Read head or chain pointer
3181
+ next_with_shift =
3182
+ h ? h->chain_next_with_shift : arr[home].head_next_with_shift;
3183
+ int shift = GetShiftFromNextWithShift(next_with_shift);
3184
+
3185
+ // Make sure it's usable
3186
+ size_t effective_home = home;
3187
+ if (UNLIKELY(shift != home_shift)) {
3188
+ // We have potentially gone awry somehow, but it's possible we're just
3189
+ // hitting old data that is not yet completed Grow.
3190
+ // NOTE: shift bits goes up with table growth.
3191
+ if (shift < home_shift) {
3192
+ // To avoid waiting on Grow in progress, an old shift amount needs
3193
+ // to be processed as if we were still using it and (potentially
3194
+ // different or the same) the old home.
3195
+ // We can assert it's not too old, because each generation of Grow
3196
+ // waits on its ancestor in the previous generation.
3197
+ assert(shift + 1 == home_shift);
3198
+ effective_home = GetHomeIndex(home, shift);
3199
+ } else if (h == read_ref_on_chain) {
3200
+ assert(shift > home_shift);
3201
+ // At head or coming from an entry on our chain where we're holding
3202
+ // a read reference. Thus, we know the newer shift applies to us.
3203
+ // Newer shift might not yet be reflected in length_info_ (an atomicity
3204
+ // gap in Grow), so operate as if it is. Note that other insertions
3205
+ // could happen using this shift before length_info_ is updated, and
3206
+ // it's possible (though unlikely) that multiple generations of Grow
3207
+ // have occurred. If shift is more than one generation ahead of
3208
+ // home_shift, it's possible that not all descendent homes have
3209
+ // reached the `shift` generation. Thus, we need to advance only one
3210
+ // shift at a time looking for a home+head with a matching shift
3211
+ // amount.
3212
+ home_shift++;
3213
+ // Update home in case it has changed
3214
+ home = GetHomeIndex(hashed_key[1], home_shift);
3215
+ // This should be rare enough occurrence that it's simplest just
3216
+ // to restart (TODO: improve in some cases?)
3217
+ h = nullptr;
3218
+ if (read_ref_on_chain) {
3219
+ Unref(*read_ref_on_chain);
3220
+ read_ref_on_chain = nullptr;
3221
+ }
3222
+ // Didn't make progress & retry
3223
+ continue;
3224
+ } else {
3225
+ assert(shift > home_shift);
3226
+ assert(h != nullptr);
3227
+ // An "under (de)construction" entry has a new shift amount, which
3228
+ // means we have either gotten off our chain or our home shift is out
3229
+ // of date. If we revert back to saved ref, we will get updated info.
3230
+ h = read_ref_on_chain;
3231
+ // Didn't make progress & retry
3232
+ continue;
3233
+ }
3234
+ }
3235
+
3236
+ // Check for end marker
3237
+ if (HandleImpl::IsEnd(next_with_shift)) {
3238
+ // To ensure we didn't miss anything in the chain, the end marker must
3239
+ // point back to the correct home.
3240
+ if (LIKELY(GetNextFromNextWithShift(next_with_shift) == effective_home)) {
3241
+ // Complete, clean iteration of the chain, not found.
3242
+ // Clean up.
3243
+ if (read_ref_on_chain) {
3244
+ Unref(*read_ref_on_chain);
3245
+ }
3246
+ return nullptr;
3247
+ } else {
3248
+ // Something went awry. Revert back to a safe point (if we have it)
3249
+ h = read_ref_on_chain;
3250
+ // Didn't make progress & retry
3251
+ continue;
3252
+ }
3253
+ }
3254
+
3255
+ // Follow the next and check for full key match, home match, or neither
3256
+ h = &arr[GetNextFromNextWithShift(next_with_shift)];
3257
+ bool full_match_or_unknown = false;
3258
+ if (MatchAndRef(&hashed_key, *h, shift, effective_home,
3259
+ &full_match_or_unknown)) {
3260
+ // Got a read ref on next (h).
3261
+ //
3262
+ // There is a very small chance that between getting the next pointer
3263
+ // (now h) and doing MatchAndRef on it, another thread erased/evicted it
3264
+ // reinserted it into the same chain, causing us to cycle back in the
3265
+ // same chain and potentially see some entries again if we keep walking.
3266
+ // Newly-inserted entries are inserted before older ones, so we are at
3267
+ // least guaranteed not to miss anything. Here in Lookup, it's just a
3268
+ // transient, slight hiccup in performance.
3269
+
3270
+ if (full_match_or_unknown) {
3271
+ // Full match.
3272
+ // Release old read ref on chain if applicable
3273
+ if (read_ref_on_chain) {
3274
+ // Pretend we never took the reference.
3275
+ Unref(*read_ref_on_chain);
3276
+ }
3277
+ // Update the hit bit
3278
+ if (eviction_callback_) {
3279
+ h->meta.fetch_or(uint64_t{1} << ClockHandle::kHitBitShift,
3280
+ std::memory_order_relaxed);
3281
+ }
3282
+ // All done.
3283
+ return h;
3284
+ } else if (UNLIKELY(shift != home_shift) &&
3285
+ home != BottomNBits(h->hashed_key[1], home_shift)) {
3286
+ // This chain is in a Grow operation and we've landed on an entry
3287
+ // that belongs to the wrong destination chain. We can keep going, but
3288
+ // there's a chance we'll need to backtrack back *before* this entry,
3289
+ // if the Grow finishes before this Lookup. We cannot save this entry
3290
+ // for backtracking because it might soon or already be on the wrong
3291
+ // chain.
3292
+ // NOTE: if we simply backtrack rather than continuing, we would
3293
+ // be in a wait loop (not allowed in Lookup!) until the other thread
3294
+ // finishes its Grow.
3295
+ Unref(*h);
3296
+ } else {
3297
+ // Correct home location, so we are on the right chain.
3298
+ // With new usable read ref, can release old one (if applicable).
3299
+ if (read_ref_on_chain) {
3300
+ // Pretend we never took the reference.
3301
+ Unref(*read_ref_on_chain);
3302
+ }
3303
+ // And keep the new one.
3304
+ read_ref_on_chain = h;
3305
+ }
3306
+ } else {
3307
+ if (full_match_or_unknown) {
3308
+ // Must have been an "under construction" entry. Can safely skip it,
3309
+ // but there's a chance we'll have to backtrack later
3310
+ } else {
3311
+ // Home mismatch! Revert back to a safe point (if we have it)
3312
+ h = read_ref_on_chain;
3313
+ // Didn't make progress & retry
3314
+ }
3315
+ }
3316
+ }
3317
+ }
3318
+
3319
+ void AutoHyperClockTable::Remove(HandleImpl* h) {
3320
+ assert((h->meta.load() >> ClockHandle::kStateShift) ==
3321
+ ClockHandle::kStateConstruction);
3322
+
3323
+ const HandleImpl& c_h = *h;
3324
+ PurgeImpl(&c_h.hashed_key);
3325
+ }
3326
+
3327
+ bool AutoHyperClockTable::TryEraseHandle(HandleImpl* h, bool holding_ref,
3328
+ bool mark_invisible) {
3329
+ uint64_t meta;
3330
+ if (mark_invisible) {
3331
+ // Set invisible
3332
+ meta = h->meta.fetch_and(
3333
+ ~(uint64_t{ClockHandle::kStateVisibleBit} << ClockHandle::kStateShift),
3334
+ std::memory_order_acq_rel);
3335
+ // To local variable also
3336
+ meta &=
3337
+ ~(uint64_t{ClockHandle::kStateVisibleBit} << ClockHandle::kStateShift);
3338
+ } else {
3339
+ meta = h->meta.load(std::memory_order_acquire);
3340
+ }
3341
+
3342
+ // Take ownership if no other refs
3343
+ do {
3344
+ if (GetRefcount(meta) != uint64_t{holding_ref}) {
3345
+ // Not last ref at some point in time during this call
3346
+ return false;
3347
+ }
3348
+ if ((meta & (uint64_t{ClockHandle::kStateShareableBit}
3349
+ << ClockHandle::kStateShift)) == 0) {
3350
+ // Someone else took ownership
3351
+ return false;
3352
+ }
3353
+ // Note that if !holding_ref, there's a small chance that we release,
3354
+ // another thread replaces this entry with another, reaches zero refs, and
3355
+ // then we end up erasing that other entry. That's an acceptable risk /
3356
+ // imprecision.
3357
+ } while (!h->meta.compare_exchange_weak(
3358
+ meta,
3359
+ uint64_t{ClockHandle::kStateConstruction} << ClockHandle::kStateShift,
3360
+ std::memory_order_acquire));
3361
+ // Took ownership
3362
+ // TODO? Delay freeing?
3363
+ h->FreeData(allocator_);
3364
+ size_t total_charge = h->total_charge;
3365
+ if (UNLIKELY(h->IsStandalone())) {
3366
+ // Delete detached handle
3367
+ delete h;
3368
+ standalone_usage_.fetch_sub(total_charge, std::memory_order_relaxed);
3369
+ } else {
3370
+ Remove(h);
3371
+ MarkEmpty(*h);
3372
+ occupancy_.fetch_sub(1U, std::memory_order_release);
3373
+ }
3374
+ usage_.fetch_sub(total_charge, std::memory_order_relaxed);
3375
+ assert(usage_.load(std::memory_order_relaxed) < SIZE_MAX / 2);
3376
+ return true;
3377
+ }
3378
+
3379
+ bool AutoHyperClockTable::Release(HandleImpl* h, bool useful,
3380
+ bool erase_if_last_ref) {
3381
+ // In contrast with LRUCache's Release, this function won't delete the handle
3382
+ // when the cache is above capacity and the reference is the last one. Space
3383
+ // is only freed up by Evict/PurgeImpl (called by Insert when space
3384
+ // is needed) and Erase. We do this to avoid an extra atomic read of the
3385
+ // variable usage_.
3386
+
3387
+ uint64_t old_meta;
3388
+ if (useful) {
3389
+ // Increment release counter to indicate was used
3390
+ old_meta = h->meta.fetch_add(ClockHandle::kReleaseIncrement,
3391
+ std::memory_order_release);
3392
+ // Correct for possible (but rare) overflow
3393
+ CorrectNearOverflow(old_meta, h->meta);
3394
+ } else {
3395
+ // Decrement acquire counter to pretend it never happened
3396
+ old_meta = h->meta.fetch_sub(ClockHandle::kAcquireIncrement,
3397
+ std::memory_order_release);
3398
+ }
3399
+
3400
+ assert((old_meta >> ClockHandle::kStateShift) &
3401
+ ClockHandle::kStateShareableBit);
3402
+ // No underflow
3403
+ assert(((old_meta >> ClockHandle::kAcquireCounterShift) &
3404
+ ClockHandle::kCounterMask) !=
3405
+ ((old_meta >> ClockHandle::kReleaseCounterShift) &
3406
+ ClockHandle::kCounterMask));
3407
+
3408
+ if ((erase_if_last_ref || UNLIKELY(old_meta >> ClockHandle::kStateShift ==
3409
+ ClockHandle::kStateInvisible))) {
3410
+ // FIXME: There's a chance here that another thread could replace this
3411
+ // entry and we end up erasing the wrong one.
3412
+ return TryEraseHandle(h, /*holding_ref=*/false, /*mark_invisible=*/false);
3413
+ } else {
3414
+ return false;
3415
+ }
3416
+ }
3417
+
3418
+ #ifndef NDEBUG
3419
+ void AutoHyperClockTable::TEST_ReleaseN(HandleImpl* h, size_t n) {
3420
+ if (n > 0) {
3421
+ // Do n-1 simple releases first
3422
+ TEST_ReleaseNMinus1(h, n);
3423
+
3424
+ // Then the last release might be more involved
3425
+ Release(h, /*useful*/ true, /*erase_if_last_ref*/ false);
3426
+ }
3427
+ }
3428
+ #endif
3429
+
3430
+ void AutoHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
3431
+ // Don't need to be efficient.
3432
+ // Might be one match masking another, so loop.
3433
+ while (HandleImpl* h = Lookup(hashed_key)) {
3434
+ bool gone =
3435
+ TryEraseHandle(h, /*holding_ref=*/true, /*mark_invisible=*/true);
3436
+ if (!gone) {
3437
+ // Only marked invisible, which is ok.
3438
+ // Pretend we never took the reference from Lookup.
3439
+ Unref(*h);
3440
+ }
3441
+ }
3442
+ }
3443
+
3444
+ void AutoHyperClockTable::EraseUnRefEntries() {
3445
+ size_t usable_size = GetTableSize();
3446
+ for (size_t i = 0; i < usable_size; i++) {
3447
+ HandleImpl& h = array_[i];
3448
+
3449
+ uint64_t old_meta = h.meta.load(std::memory_order_relaxed);
3450
+ if (old_meta & (uint64_t{ClockHandle::kStateShareableBit}
3451
+ << ClockHandle::kStateShift) &&
3452
+ GetRefcount(old_meta) == 0 &&
3453
+ h.meta.compare_exchange_strong(old_meta,
3454
+ uint64_t{ClockHandle::kStateConstruction}
3455
+ << ClockHandle::kStateShift,
3456
+ std::memory_order_acquire)) {
3457
+ // Took ownership
3458
+ h.FreeData(allocator_);
3459
+ usage_.fetch_sub(h.total_charge, std::memory_order_relaxed);
3460
+ // NOTE: could be more efficient with a dedicated variant of
3461
+ // PurgeImpl, but this is not a common operation
3462
+ Remove(&h);
3463
+ MarkEmpty(h);
3464
+ occupancy_.fetch_sub(1U, std::memory_order_release);
3465
+ }
3466
+ }
3467
+ }
3468
+
3469
+ void AutoHyperClockTable::Evict(size_t requested_charge, InsertState& state,
3470
+ EvictionData* data) {
3471
+ // precondition
3472
+ assert(requested_charge > 0);
3473
+
3474
+ // We need the clock pointer to seemlessly "wrap around" at the end of the
3475
+ // table, and to be reasonably stable under Grow operations. This is
3476
+ // challenging when the linear hashing progressively opens additional
3477
+ // most-significant-hash-bits in determining home locations.
3478
+
3479
+ // TODO: make a tuning parameter?
3480
+ // Up to 2x this number of homes will be evicted per step. In very rare
3481
+ // cases, possibly more, as homes of an out-of-date generation will be
3482
+ // resolved to multiple in a newer generation.
3483
+ constexpr size_t step_size = 4;
3484
+
3485
+ // A clock_pointer_mask_ field separate from length_info_ enables us to use
3486
+ // the same mask (way of dividing up the space among evicting threads) for
3487
+ // iterating over the whole structure before considering changing the mask
3488
+ // at the beginning of each pass. This ensures we do not have a large portion
3489
+ // of the space that receives redundant or missed clock updates. However,
3490
+ // with two variables, for each update to clock_pointer_mask (< 64 ever in
3491
+ // the life of the cache), there will be a brief period where concurrent
3492
+ // eviction threads could use the old mask value, possibly causing redundant
3493
+ // or missed clock updates for a *small* portion of the table.
3494
+ size_t clock_pointer_mask =
3495
+ clock_pointer_mask_.load(std::memory_order_relaxed);
3496
+
3497
+ uint64_t max_clock_pointer = 0; // unset
3498
+
3499
+ // TODO: consider updating during a long eviction
3500
+ size_t used_length = LengthInfoToUsedLength(state.saved_length_info);
3501
+
3502
+ autovector<HandleImpl*> to_finish_eviction;
3503
+
3504
+ // Loop until enough freed, or limit reached (see bottom of loop)
3505
+ for (;;) {
3506
+ // First (concurrent) increment clock pointer
3507
+ uint64_t old_clock_pointer =
3508
+ clock_pointer_.fetch_add(step_size, std::memory_order_relaxed);
3509
+
3510
+ if (UNLIKELY((old_clock_pointer & clock_pointer_mask) == 0)) {
3511
+ // Back at the beginning. See if clock_pointer_mask should be updated.
3512
+ uint64_t mask = BottomNBits(
3513
+ UINT64_MAX, LengthInfoToMinShift(state.saved_length_info));
3514
+ if (clock_pointer_mask != mask) {
3515
+ clock_pointer_mask = static_cast<size_t>(mask);
3516
+ clock_pointer_mask_.store(clock_pointer_mask,
3517
+ std::memory_order_relaxed);
3518
+ }
3519
+ }
3520
+
3521
+ size_t major_step = clock_pointer_mask + 1;
3522
+ assert((major_step & clock_pointer_mask) == 0);
3523
+
3524
+ for (size_t base_home = old_clock_pointer & clock_pointer_mask;
3525
+ base_home < used_length; base_home += major_step) {
3526
+ for (size_t i = 0; i < step_size; i++) {
3527
+ size_t home = base_home + i;
3528
+ if (home >= used_length) {
3529
+ break;
3530
+ }
3531
+ PurgeImpl(&to_finish_eviction, home);
3532
+ }
3533
+ }
3534
+
3535
+ for (HandleImpl* h : to_finish_eviction) {
3536
+ TrackAndReleaseEvictedEntry(h, data);
3537
+ // NOTE: setting likely_empty_slot here can cause us to reduce the
3538
+ // portion of "at home" entries, probably because an evicted entry
3539
+ // is more likely to come back than a random new entry and would be
3540
+ // unable to go into its home slot.
3541
+ }
3542
+ to_finish_eviction.clear();
3543
+
3544
+ // Loop exit conditions
3545
+ if (data->freed_charge >= requested_charge) {
3546
+ return;
3547
+ }
3548
+
3549
+ if (max_clock_pointer == 0) {
3550
+ // Cap the eviction effort at this thread (along with those operating in
3551
+ // parallel) circling through the whole structure kMaxCountdown times.
3552
+ // In other words, this eviction run must find something/anything that is
3553
+ // unreferenced at start of and during the eviction run that isn't
3554
+ // reclaimed by a concurrent eviction run.
3555
+ // TODO: Does HyperClockCache need kMaxCountdown + 1?
3556
+ max_clock_pointer =
3557
+ old_clock_pointer +
3558
+ (uint64_t{ClockHandle::kMaxCountdown + 1} * major_step);
3559
+ }
3560
+
3561
+ if (old_clock_pointer + step_size >= max_clock_pointer) {
3562
+ return;
3563
+ }
3564
+ }
3565
+ }
3566
+
3567
+ size_t AutoHyperClockTable::CalcMaxUsableLength(
3568
+ size_t capacity, size_t min_avg_value_size,
3569
+ CacheMetadataChargePolicy metadata_charge_policy) {
3570
+ double min_avg_slot_charge = min_avg_value_size * kMaxLoadFactor;
3571
+ if (metadata_charge_policy == kFullChargeCacheMetadata) {
3572
+ min_avg_slot_charge += sizeof(HandleImpl);
3573
+ }
3574
+ assert(min_avg_slot_charge > 0.0);
3575
+ size_t num_slots =
3576
+ static_cast<size_t>(capacity / min_avg_slot_charge + 0.999999);
3577
+
3578
+ const size_t slots_per_page = port::kPageSize / sizeof(HandleImpl);
3579
+
3580
+ // Round up to page size
3581
+ return ((num_slots + slots_per_page - 1) / slots_per_page) * slots_per_page;
3582
+ }
3583
+
3584
+ namespace {
3585
+ bool IsHeadNonempty(const AutoHyperClockTable::HandleImpl& h) {
3586
+ return !AutoHyperClockTable::HandleImpl::IsEnd(
3587
+ h.head_next_with_shift.load(std::memory_order_relaxed));
3588
+ }
3589
+ bool IsEntryAtHome(const AutoHyperClockTable::HandleImpl& h, int shift,
3590
+ size_t home) {
3591
+ if (MatchAndRef(nullptr, h, shift, home)) {
3592
+ Unref(h);
3593
+ return true;
3594
+ } else {
3595
+ return false;
3596
+ }
3597
+ }
3598
+ } // namespace
3599
+
3600
+ void AutoHyperClockCache::ReportProblems(
3601
+ const std::shared_ptr<Logger>& info_log) const {
3602
+ BaseHyperClockCache::ReportProblems(info_log);
3603
+
3604
+ if (info_log->GetInfoLogLevel() <= InfoLogLevel::DEBUG_LEVEL) {
3605
+ LoadVarianceStats head_stats;
3606
+ size_t entry_at_home_count = 0;
3607
+ uint64_t yield_count = 0;
3608
+ this->ForEachShard([&](const Shard* shard) {
3609
+ size_t count = shard->GetTableAddressCount();
3610
+ uint64_t length_info = UsedLengthToLengthInfo(count);
3611
+ for (size_t i = 0; i < count; ++i) {
3612
+ const auto& h = *shard->GetTable().HandlePtr(i);
3613
+ head_stats.Add(IsHeadNonempty(h));
3614
+ int shift;
3615
+ size_t home;
3616
+ GetHomeIndexAndShift(length_info, i, &home, &shift);
3617
+ assert(home == i);
3618
+ entry_at_home_count += IsEntryAtHome(h, shift, home);
3619
+ }
3620
+ yield_count += shard->GetTable().GetYieldCount();
3621
+ });
3622
+ ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
3623
+ "Head occupancy stats: %s", head_stats.Report().c_str());
3624
+ ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
3625
+ "Entries at home count: %zu", entry_at_home_count);
3626
+ ROCKS_LOG_AT_LEVEL(info_log, InfoLogLevel::DEBUG_LEVEL,
3627
+ "Yield count: %" PRIu64, yield_count);
3628
+ }
3629
+ }
3630
+
3631
+ } // namespace clock_cache
3632
+
3633
+ // DEPRECATED (see public API)
3634
+ std::shared_ptr<Cache> NewClockCache(
3635
+ size_t capacity, int num_shard_bits, bool strict_capacity_limit,
3636
+ CacheMetadataChargePolicy metadata_charge_policy) {
3637
+ return NewLRUCache(capacity, num_shard_bits, strict_capacity_limit,
3638
+ /* high_pri_pool_ratio */ 0.5, nullptr,
3639
+ kDefaultToAdaptiveMutex, metadata_charge_policy,
3640
+ /* low_pri_pool_ratio */ 0.0);
3641
+ }
3642
+
3643
+ std::shared_ptr<Cache> HyperClockCacheOptions::MakeSharedCache() const {
3644
+ // For sanitized options
3645
+ HyperClockCacheOptions opts = *this;
3646
+ if (opts.num_shard_bits >= 20) {
3647
+ return nullptr; // The cache cannot be sharded into too many fine pieces.
3648
+ }
3649
+ if (opts.num_shard_bits < 0) {
3650
+ // Use larger shard size to reduce risk of large entries clustering
3651
+ // or skewing individual shards.
3652
+ constexpr size_t min_shard_size = 32U * 1024U * 1024U;
3653
+ opts.num_shard_bits =
3654
+ GetDefaultCacheShardBits(opts.capacity, min_shard_size);
3655
+ }
3656
+ std::shared_ptr<Cache> cache;
3657
+ if (opts.estimated_entry_charge == 0) {
3658
+ cache = std::make_shared<clock_cache::AutoHyperClockCache>(opts);
3659
+ } else {
3660
+ cache = std::make_shared<clock_cache::FixedHyperClockCache>(opts);
1472
3661
  }
1473
- std::shared_ptr<Cache> cache =
1474
- std::make_shared<clock_cache::HyperClockCache>(opts);
1475
3662
  if (opts.secondary_cache) {
1476
3663
  cache = std::make_shared<CacheWithSecondaryAdapter>(cache,
1477
3664
  opts.secondary_cache);