raw_hashtable_benchmark_helpers.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. // Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  2. // Exceptions. See /LICENSE for license information.
  3. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  4. #include "common/raw_hashtable_benchmark_helpers.h"
  5. #include <array>
  6. #include <cstddef>
  7. #include <forward_list>
  8. #include <utility>
  9. #include <vector>
  10. namespace Carbon::RawHashtable {
  11. // A local shuffle implementation built on Abseil to improve performance in
  12. // debug builds.
  13. template <typename T>
  14. static auto Shuffle(llvm::MutableArrayRef<T> data, absl::BitGen& gen) {
  15. for (ssize_t i : llvm::seq<ssize_t>(0, data.size() - 1)) {
  16. ssize_t j = absl::Uniform<ssize_t>(gen, 0, data.size() - i);
  17. if (j != 0) {
  18. std::swap(data[i], data[i + j]);
  19. }
  20. }
  21. }
  22. constexpr ssize_t NumChars = 64;
  23. static_assert(llvm::isPowerOf2_64(NumChars));
  24. // For benchmarking, we use short strings in a fixed distribution with common
  25. // characters. Real-world strings aren't uniform across ASCII or Unicode, etc.
  26. // And for *micro*-benchmarking we want to focus on the map overhead with short,
  27. // fast keys.
  28. static auto MakeChars() -> llvm::SmallVector<char> {
  29. llvm::SmallVector<char> characters;
  30. characters.reserve(NumChars);
  31. // Start with `-` and `_`, and then add `a` - `z`, `A` - `Z`, and `0` - `9`.
  32. characters.push_back('-');
  33. characters.push_back('_');
  34. llvm::append_range(characters, llvm::seq_inclusive('a', 'z'));
  35. llvm::append_range(characters, llvm::seq_inclusive('A', 'Z'));
  36. llvm::append_range(characters, llvm::seq_inclusive('0', '9'));
  37. CARBON_CHECK(characters.size() == NumChars,
  38. "Expected exactly {0} characters, got {1} instead!", NumChars,
  39. characters.size());
  40. return characters;
  41. }
  42. constexpr ssize_t NumFourCharStrs = NumChars * NumChars * NumChars * NumChars;
  43. static_assert(llvm::isPowerOf2_64(NumFourCharStrs));
  44. // Compute every 4-character string in a shuffled array. This is a little memory
  45. // intense -- 64 MiB -- but ends up being much cheaper by letting us reliably
  46. // select a unique 4-character sequence to avoid collisions.
  47. static auto MakeFourCharStrs(llvm::ArrayRef<char> characters, absl::BitGen& gen)
  48. -> llvm::SmallVector<std::array<char, 4>> {
  49. constexpr ssize_t NumCharsMask = NumChars - 1;
  50. constexpr ssize_t NumCharsShift = llvm::ConstantLog2<NumChars>();
  51. llvm::SmallVector<std::array<char, 4>> four_char_strs(
  52. llvm::map_range(llvm::seq(NumFourCharStrs), [&](auto i) {
  53. std::array<char, 4> str;
  54. str[0] = characters[i & NumCharsMask];
  55. i >>= NumCharsShift;
  56. str[1] = characters[i & NumCharsMask];
  57. i >>= NumCharsShift;
  58. str[2] = characters[i & NumCharsMask];
  59. i >>= NumCharsShift;
  60. CARBON_CHECK((i & ~NumCharsMask) == 0);
  61. str[3] = characters[i];
  62. return str;
  63. }));
  64. Shuffle<std::array<char, 4>>(four_char_strs, gen);
  65. return four_char_strs;
  66. }
  67. constexpr ssize_t NumRandomChars = static_cast<ssize_t>(64) * 1024;
  68. // Create a pool of random characters to sample from rather than computing this
  69. // for every string which is very slow in debug builds. We also pad this pool
  70. // with the max length so we can pull the full length from the end to simplify
  71. // the logic when wrapping around the pool.
  72. static auto MakeRandomChars(llvm::ArrayRef<char> characters, int max_length,
  73. absl::BitGen& gen) -> llvm::SmallVector<char> {
  74. return llvm::SmallVector<char>(
  75. llvm::map_range(llvm::seq(NumRandomChars + max_length), [&](auto /*i*/) {
  76. return characters[absl::Uniform<ssize_t>(gen, 0, NumChars)];
  77. }));
  78. }
  79. // Make a small vector of pointers into a single allocation of raw strings. The
  80. // allocated memory is expected to leak and must be transitively referenced by a
  81. // global. Each string has `length` size (which must be >= 4), and there are
  82. // `key_count` keys in the result. Each key is filled from the `random_chars`
  83. // until the last 4 characters. The last four characters of each string will be
  84. // taken sequentially from `four_char_strs` from some random start position to
  85. // ensure no duplicate keys are produced.
  86. static auto MakeRawStrKeys(ssize_t length, ssize_t key_count,
  87. llvm::ArrayRef<std::array<char, 4>> four_char_strs,
  88. llvm::ArrayRef<char> random_chars, absl::BitGen& gen)
  89. -> llvm::SmallVector<const char*> {
  90. llvm::SmallVector<const char*> raw_keys;
  91. CARBON_CHECK(length >= 4);
  92. ssize_t prefix_length = length - 4;
  93. // Select a random start for indexing our four character strings.
  94. ssize_t four_char_index = absl::Uniform<ssize_t>(gen, 0, NumFourCharStrs);
  95. // Select a random start for the prefix random characters.
  96. ssize_t random_chars_index = absl::Uniform<ssize_t>(gen, 0, NumRandomChars);
  97. // Do a single memory allocation for all the keys of this length to
  98. // avoid an excessive number of small and fragmented allocations. This
  99. // memory is intentionally leaked as the keys are global and will
  100. // themselves will point into it.
  101. char* key_text = new char[key_count * length];
  102. // Reserve all the key space since we know how many we'll need.
  103. raw_keys.reserve(key_count);
  104. for ([[gnu::unused]] ssize_t i : llvm::seq<ssize_t>(0, key_count)) {
  105. memcpy(key_text, random_chars.data() + random_chars_index, prefix_length);
  106. random_chars_index += prefix_length;
  107. random_chars_index &= NumRandomChars - 1;
  108. // Set the last four characters with this entry in the shuffled
  109. // sequence.
  110. memcpy(key_text + prefix_length, four_char_strs[four_char_index].data(), 4);
  111. // Step through the shuffled sequence. We start at a random position,
  112. // so we need to wrap around the end.
  113. ++four_char_index;
  114. four_char_index &= NumFourCharStrs - 1;
  115. // And finally save the start pointer as one of our raw keys.
  116. raw_keys.push_back(key_text);
  117. key_text += length;
  118. }
  119. return raw_keys;
  120. }
  121. // Build up a large collection of random and unique string keys. This is
  122. // actually a relatively expensive operation due to needing to build all the
  123. // random string text. As a consequence, the initializer of this global is
  124. // somewhat performance tuned to ensure benchmarks don't take an excessive
  125. // amount of time to run or use an excessive amount of memory.
  126. static absl::NoDestructor<llvm::SmallVector<llvm::StringRef>> raw_str_keys{[] {
  127. llvm::SmallVector<llvm::StringRef> keys(MaxNumKeys);
  128. absl::BitGen gen;
  129. std::array length_buckets = {
  130. 4, 4, 4, 4, 5, 5, 5, 5, 7, 7, 10, 10, 15, 25, 40, 80,
  131. };
  132. static_assert((MaxNumKeys % length_buckets.size()) == 0);
  133. CARBON_CHECK(llvm::is_sorted(length_buckets));
  134. // For each distinct length bucket, we build a vector of raw keys.
  135. std::forward_list<llvm::SmallVector<const char*>> raw_keys_storage;
  136. // And a parallel array to the length buckets with the raw keys of that
  137. // length.
  138. std::array<llvm::SmallVector<const char*>*, length_buckets.size()>
  139. raw_keys_buckets;
  140. llvm::SmallVector<char> characters = MakeChars();
  141. llvm::SmallVector<std::array<char, 4>> four_char_strs =
  142. MakeFourCharStrs(characters, gen);
  143. llvm::SmallVector<char> random_chars =
  144. MakeRandomChars(characters, /*max_length=*/length_buckets.back(), gen);
  145. ssize_t prev_length = -1;
  146. for (auto [length_index, length] : llvm::enumerate(length_buckets)) {
  147. // We can detect repetitions in length as they are sorted.
  148. if (length == prev_length) {
  149. raw_keys_buckets[length_index] = raw_keys_buckets[length_index - 1];
  150. continue;
  151. }
  152. prev_length = length;
  153. // We want to compute all the keys of this length that we'll need.
  154. ssize_t key_count = (MaxNumKeys / length_buckets.size()) *
  155. llvm::count(length_buckets, length);
  156. raw_keys_buckets[length_index] = &raw_keys_storage.emplace_front(
  157. MakeRawStrKeys(length, key_count, four_char_strs, random_chars, gen));
  158. }
  159. // Now build the actual key array from our intermediate storage by
  160. // round-robin extracting from the length buckets.
  161. for (auto [index, key] : llvm::enumerate(keys)) {
  162. ssize_t bucket = index % length_buckets.size();
  163. ssize_t length = length_buckets[bucket];
  164. // We pop a raw key from the list of them associated with this bucket.
  165. const char* raw_key = raw_keys_buckets[bucket]->pop_back_val();
  166. // And build our key from that.
  167. key = llvm::StringRef(raw_key, length);
  168. }
  169. // Check that in fact we popped every raw key into our main keys.
  170. for (const auto& raw_keys : raw_keys_storage) {
  171. CARBON_CHECK(raw_keys.empty());
  172. }
  173. return keys;
  174. }()};
  175. static absl::NoDestructor<llvm::SmallVector<int*>> raw_ptr_keys(llvm::map_range(
  176. llvm::seq(MaxNumKeys), [](int index) { return new int{index}; }));
  177. static absl::NoDestructor<llvm::SmallVector<int>> raw_int_keys(llvm::map_range(
  178. llvm::seq(MaxNumKeys), [](int index) { return index + 1; }));
  179. template <int LowZeroBits>
  180. static absl::NoDestructor<llvm::SmallVector<LowZeroBitInt<LowZeroBits>>>
  181. raw_low_zero_bit_int_keys(
  182. llvm::map_range(llvm::seq(MaxNumKeys), [](int index) {
  183. return LowZeroBitInt<LowZeroBits>(index + 1);
  184. }));
  185. namespace {
  186. // Allow generically dispatching over the specific key types we build and
  187. // support.
  188. template <typename T>
  189. auto GetRawKeys() -> llvm::ArrayRef<T> {
  190. if constexpr (std::is_same_v<T, llvm::StringRef>) {
  191. return *raw_str_keys;
  192. } else if constexpr (std::is_pointer_v<T>) {
  193. return *raw_ptr_keys;
  194. } else if constexpr (std::is_same_v<T, LowZeroBitInt<12>>) {
  195. return *raw_low_zero_bit_int_keys<12>;
  196. } else if constexpr (std::is_same_v<T, LowZeroBitInt<24>>) {
  197. return *raw_low_zero_bit_int_keys<24>;
  198. } else if constexpr (std::is_same_v<T, LowZeroBitInt<32>>) {
  199. return *raw_low_zero_bit_int_keys<32>;
  200. } else {
  201. return *raw_int_keys;
  202. }
  203. }
  204. template <typename T>
  205. static absl::NoDestructor<
  206. std::map<std::pair<ssize_t, ssize_t>, llvm::SmallVector<T>>>
  207. lookup_keys_storage;
  208. // Given a particular table keys size and lookup keys size, provide an array ref
  209. // to a shuffled set of lookup keys.
  210. //
  211. // Because different table sizes pull from different sub-ranges of our raw keys,
  212. // we need to compute a distinct set of random keys in the table to use for
  213. // lookups depending on the table size. And we also want to have an even
  214. // distribution of key *sizes* throughout the lookup keys, and so we can't
  215. // compute a single lookup keys array of the maximum size. Instead we need to
  216. // compute a distinct special set of lookup keys for each pair of table and
  217. // lookup size, and then shuffle that specific set into a random sequence that
  218. // is returned. This function memoizes this sequence for each pair of sizes.
  219. template <typename T>
  220. auto GetShuffledLookupKeys(ssize_t table_keys_size, ssize_t lookup_keys_size)
  221. -> llvm::ArrayRef<T> {
  222. // The raw keys aren't shuffled and round-robin through the sizes. We want to
  223. // keep the total size of lookup keys used exactly the same across runs. So
  224. // for a given size we always take the leading sequence from the raw keys for
  225. // that size, duplicating as needed to get the desired lookup sequence size,
  226. // and then shuffle the keys in that sequence to end up with a random sequence
  227. // of keys. We store each of these shuffled sequences in a map to avoid
  228. // repeatedly computing them.
  229. llvm::SmallVector<T>& lookup_keys =
  230. (*lookup_keys_storage<T>)[{table_keys_size, lookup_keys_size}];
  231. if (lookup_keys.empty()) {
  232. auto raw_keys = GetRawKeys<T>();
  233. llvm::append_range(
  234. lookup_keys,
  235. llvm::map_range(llvm::seq(lookup_keys_size), [&](int index) {
  236. return raw_keys[index % table_keys_size];
  237. }));
  238. absl::BitGen gen;
  239. Shuffle<T>(lookup_keys, gen);
  240. }
  241. CARBON_CHECK(static_cast<ssize_t>(lookup_keys.size()) == lookup_keys_size);
  242. return lookup_keys;
  243. }
  244. } // namespace
  245. template <typename T>
  246. auto GetKeysAndMissKeys(ssize_t table_keys_size)
  247. -> std::pair<llvm::ArrayRef<T>, llvm::ArrayRef<T>> {
  248. CARBON_CHECK(table_keys_size <= MaxNumKeys);
  249. // The raw keys aren't shuffled and round-robin through the sizes. Take the
  250. // tail of this sequence and shuffle it to form a random set of miss keys with
  251. // a consistent total size.
  252. static absl::NoDestructor<llvm::SmallVector<T>> miss_keys{[] {
  253. llvm::SmallVector<T> keys(GetRawKeys<T>().take_back(NumOtherKeys));
  254. CARBON_CHECK(keys.size() == NumOtherKeys);
  255. absl::BitGen gen;
  256. Shuffle<T>(keys, gen);
  257. return keys;
  258. }()};
  259. return {GetRawKeys<T>().slice(0, table_keys_size), *miss_keys};
  260. }
  261. template auto GetKeysAndMissKeys<int>(ssize_t size)
  262. -> std::pair<llvm::ArrayRef<int>, llvm::ArrayRef<int>>;
  263. template auto GetKeysAndMissKeys<int*>(ssize_t size)
  264. -> std::pair<llvm::ArrayRef<int*>, llvm::ArrayRef<int*>>;
  265. template auto GetKeysAndMissKeys<llvm::StringRef>(ssize_t size)
  266. -> std::pair<llvm::ArrayRef<llvm::StringRef>,
  267. llvm::ArrayRef<llvm::StringRef>>;
  268. template auto GetKeysAndMissKeys<LowZeroBitInt<12>>(ssize_t size)
  269. -> std::pair<llvm::ArrayRef<LowZeroBitInt<12>>,
  270. llvm::ArrayRef<LowZeroBitInt<12>>>;
  271. template auto GetKeysAndMissKeys<LowZeroBitInt<24>>(ssize_t size)
  272. -> std::pair<llvm::ArrayRef<LowZeroBitInt<24>>,
  273. llvm::ArrayRef<LowZeroBitInt<24>>>;
  274. template auto GetKeysAndMissKeys<LowZeroBitInt<32>>(ssize_t size)
  275. -> std::pair<llvm::ArrayRef<LowZeroBitInt<32>>,
  276. llvm::ArrayRef<LowZeroBitInt<32>>>;
  277. template <typename T>
  278. auto GetKeysAndHitKeys(ssize_t table_keys_size, ssize_t lookup_keys_size)
  279. -> std::pair<llvm::ArrayRef<T>, llvm::ArrayRef<T>> {
  280. CARBON_CHECK(table_keys_size <= MaxNumKeys);
  281. CARBON_CHECK(lookup_keys_size <= MaxNumKeys);
  282. return {GetRawKeys<T>().slice(0, table_keys_size),
  283. GetShuffledLookupKeys<T>(table_keys_size, lookup_keys_size)};
  284. }
  285. template auto GetKeysAndHitKeys<int>(ssize_t size, ssize_t lookup_keys_size)
  286. -> std::pair<llvm::ArrayRef<int>, llvm::ArrayRef<int>>;
  287. template auto GetKeysAndHitKeys<int*>(ssize_t size, ssize_t lookup_keys_size)
  288. -> std::pair<llvm::ArrayRef<int*>, llvm::ArrayRef<int*>>;
  289. template auto GetKeysAndHitKeys<llvm::StringRef>(ssize_t size,
  290. ssize_t lookup_keys_size)
  291. -> std::pair<llvm::ArrayRef<llvm::StringRef>,
  292. llvm::ArrayRef<llvm::StringRef>>;
  293. template auto GetKeysAndHitKeys<LowZeroBitInt<12>>(ssize_t size,
  294. ssize_t lookup_keys_size)
  295. -> std::pair<llvm::ArrayRef<LowZeroBitInt<12>>,
  296. llvm::ArrayRef<LowZeroBitInt<12>>>;
  297. template auto GetKeysAndHitKeys<LowZeroBitInt<24>>(ssize_t size,
  298. ssize_t lookup_keys_size)
  299. -> std::pair<llvm::ArrayRef<LowZeroBitInt<24>>,
  300. llvm::ArrayRef<LowZeroBitInt<24>>>;
  301. template auto GetKeysAndHitKeys<LowZeroBitInt<32>>(ssize_t size,
  302. ssize_t lookup_keys_size)
  303. -> std::pair<llvm::ArrayRef<LowZeroBitInt<32>>,
  304. llvm::ArrayRef<LowZeroBitInt<32>>>;
  305. template <typename T>
  306. auto DumpHashStatistics(llvm::ArrayRef<T> keys) -> void {
  307. if (keys.size() < GroupSize) {
  308. return;
  309. }
  310. // The hash table load factor is 7/8ths, so we want to add 1/7th of our
  311. // current size, subtract one, and pick the next power of two to get the power
  312. // of two where 7/8ths is greater than or equal to the incoming key size.
  313. ssize_t expected_size =
  314. llvm::NextPowerOf2(keys.size() + (keys.size() / 7) - 1);
  315. constexpr int GroupShift = llvm::ConstantLog2<GroupSize>();
  316. size_t mask = ComputeProbeMaskFromSize(expected_size);
  317. uint64_t salt = ComputeSeed();
  318. auto get_hash_index = [mask, salt](auto x) -> ssize_t {
  319. auto [hash_index, _] = HashValue(x, salt).template ExtractIndexAndTag<7>();
  320. return (hash_index & mask) >> GroupShift;
  321. };
  322. std::vector<std::vector<int>> grouped_key_indices(expected_size >>
  323. GroupShift);
  324. for (auto [i, k] : llvm::enumerate(keys)) {
  325. ssize_t hash_index = get_hash_index(k);
  326. CARBON_CHECK(hash_index < (expected_size >> GroupShift), "{0}", hash_index);
  327. grouped_key_indices[hash_index].push_back(i);
  328. }
  329. ssize_t max_group_index =
  330. llvm::max_element(grouped_key_indices,
  331. [](const auto& lhs, const auto& rhs) {
  332. return lhs.size() < rhs.size();
  333. }) -
  334. grouped_key_indices.begin();
  335. // If the max number of collisions on the index is less than or equal to the
  336. // group size, there shouldn't be any necessary probing (outside of deletion)
  337. // and so this isn't interesting, skip printing.
  338. if (grouped_key_indices[max_group_index].size() <= GroupSize) {
  339. return;
  340. }
  341. llvm::errs() << "keys: " << keys.size()
  342. << " groups: " << grouped_key_indices.size() << "\n"
  343. << "max group index: " << llvm::formatv("{0x8}", max_group_index)
  344. << " collisions: "
  345. << grouped_key_indices[max_group_index].size() << "\n";
  346. for (auto i : llvm::ArrayRef(grouped_key_indices[max_group_index])
  347. .take_front(2 * GroupSize)) {
  348. auto k = keys[i];
  349. auto hash = static_cast<uint64_t>(HashValue(k, salt));
  350. llvm::errs() << " key: " << k
  351. << " salt: " << llvm::formatv("{0:x16}", salt)
  352. << " hash: " << llvm::formatv("{0:x16}", hash) << "\n";
  353. }
  354. }
  355. template auto DumpHashStatistics(llvm::ArrayRef<int> keys) -> void;
  356. template auto DumpHashStatistics(llvm::ArrayRef<LowZeroBitInt<12>> keys)
  357. -> void;
  358. template auto DumpHashStatistics(llvm::ArrayRef<LowZeroBitInt<24>> keys)
  359. -> void;
  360. template auto DumpHashStatistics(llvm::ArrayRef<LowZeroBitInt<32>> keys)
  361. -> void;
  362. template auto DumpHashStatistics(llvm::ArrayRef<int*> keys) -> void;
  363. template auto DumpHashStatistics(llvm::ArrayRef<llvm::StringRef> keys) -> void;
  364. } // namespace Carbon::RawHashtable