blob: e0a7a11e68095ace54ee821f2c15f87b3b75ea73 [file] [edit]
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
// Compile (from $WEBKIT_ROOT, after a mac-dev-release build):
// xcrun clang++ -o HashSetVariantsBenchmark \
// Source/WTF/benchmarks/HashSetVariantsBenchmark.cpp \
// -std=c++23 -O3 -DNDEBUG=1 -fno-exceptions -fno-rtti \
// -ISource/WTF \
// -IWebKitBuild/cmake-mac/Release \
// -IWebKitBuild/cmake-mac/Release/WTF/DerivedSources \
// -FWebKitBuild/cmake-mac/Release \
// -framework JavaScriptCore -framework Foundation
//
// Run:
// DYLD_FRAMEWORK_PATH=WebKitBuild/cmake-mac/Release ./HashSetVariantsBenchmark
//
// Optional first arg: filter substring (matches container, op, or key type).
// Optional second arg: a multiplier for repetitions (default 1.0).
#include "config.h"
#include <wtf/Atomics.h>
#include <wtf/DataLog.h>
#include <wtf/HashSet.h>
#include <wtf/ListHashSet.h>
#include <wtf/MonotonicTime.h>
#include <wtf/OrderedHashSet.h>
#include <wtf/StdLibExtras.h>
#include <wtf/Threading.h>
#include <wtf/Vector.h>
#include <wtf/text/StringHash.h>
#include <wtf/text/WTFString.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace {
// Sizes are chosen so all three containers settle at the SAME bucket count
// and SAME load factor (~50%) after construction. Power-of-two sizes hit
// HashTable's post-insert expansion edge (final load 25%) and
// OrderedHashTable's pre-insert expansion edge (final load 50%) — different
// loads, so lookup measurements compare layouts AND probe counts. Sizes of
// 2^k - 1 sit just below both expansion triggers, equalizing both.
constexpr unsigned kCapacities[] = { 7, 63, 511, 4095, 32767, 262143 };
const char* g_filter = nullptr;
double g_repMultiplier = 1.0;
struct Row {
const char* container;
const char* op;
const char* keyType;
unsigned n;
double nsPerOp;
};
WTF::Vector<Row> g_rows;
bool matchesFilter(const char* container, const char* op, const char* keyType)
{
if (!g_filter)
return true;
auto contains = [](const char* haystack, const char* needle) {
return strstr(haystack, needle) != nullptr;
};
return contains(container, g_filter) || contains(op, g_filter) || contains(keyType, g_filter);
}
unsigned scaledReps(unsigned baseReps)
{
double scaled = baseReps * g_repMultiplier;
if (scaled < 1.0)
scaled = 1.0;
return static_cast<unsigned>(scaled);
}
// Reps tuned so each cell takes roughly 50-300ms wall time.
unsigned repsForInsert(unsigned n)
{
if (n <= 8) return scaledReps(2000000);
if (n <= 64) return scaledReps(300000);
if (n <= 512) return scaledReps(40000);
if (n <= 4096) return scaledReps(4000);
if (n <= 32768) return scaledReps(400);
return scaledReps(40);
}
unsigned repsForLookup(unsigned n)
{
if (n <= 8) return scaledReps(8000000);
if (n <= 64) return scaledReps(1000000);
if (n <= 512) return scaledReps(100000);
if (n <= 4096) return scaledReps(10000);
if (n <= 32768) return scaledReps(1000);
return scaledReps(100);
}
unsigned repsForIterate(unsigned n)
{
if (n <= 8) return scaledReps(8000000);
if (n <= 64) return scaledReps(1000000);
if (n <= 512) return scaledReps(100000);
if (n <= 4096) return scaledReps(10000);
if (n <= 32768) return scaledReps(1000);
return scaledReps(100);
}
unsigned repsForChurn(unsigned n)
{
if (n <= 8) return scaledReps(2000000);
if (n <= 64) return scaledReps(300000);
if (n <= 512) return scaledReps(40000);
if (n <= 4096) return scaledReps(4000);
if (n <= 32768) return scaledReps(400);
return scaledReps(40);
}
template<typename T> T makeKey(unsigned i);
template<> uint32_t makeKey<uint32_t>(unsigned i) { return i + 1; }
template<> void* makeKey<void*>(unsigned i)
{
// Multiply to spread bits more like real heap pointers, and avoid 0.
return std::bit_cast<void*>(static_cast<uintptr_t>((i + 1) * 16));
}
template<> WTF::String makeKey<WTF::String>(unsigned i)
{
return WTF::String::number(i + 1);
}
template<typename T>
WTF::Vector<T> makeKeys(unsigned n)
{
WTF::Vector<T> v;
v.reserveInitialCapacity(n);
for (unsigned i = 0; i < n; ++i)
v.append(makeKey<T>(i));
return v;
}
// To avoid stale-pointer hazards with WTF::String, keep present strings around
// for the lifetime of "non-present" lookups too. We don't lookup absent keys
// here, but the scaffold supports it.
template<template<typename...> class SetTemplate, typename T>
NEVER_INLINE void benchInsert(unsigned n, const char* container, const char* keyType)
{
if (!matchesFilter(container, "Insert", keyType))
return;
auto keys = makeKeys<T>(n);
unsigned reps = repsForInsert(n);
auto before = WTF::MonotonicTime::now();
for (unsigned r = 0; r < reps; ++r) {
SetTemplate<T> set;
for (unsigned i = 0; i < n; ++i)
set.add(keys[i]);
WTF::compilerFence();
}
auto after = WTF::MonotonicTime::now();
double totalNs = (after - before).nanoseconds();
g_rows.append({ container, "Insert", keyType, n, totalNs / (static_cast<double>(reps) * n) });
}
template<template<typename...> class SetTemplate, typename T>
NEVER_INLINE void benchContainsHit(unsigned n, const char* container, const char* keyType)
{
if (!matchesFilter(container, "ContainsHit", keyType))
return;
auto keys = makeKeys<T>(n);
SetTemplate<T> set;
for (unsigned i = 0; i < n; ++i)
set.add(keys[i]);
unsigned reps = repsForLookup(n);
unsigned hitCount = 0;
auto before = WTF::MonotonicTime::now();
for (unsigned r = 0; r < reps; ++r) {
for (unsigned i = 0; i < n; ++i) {
if (set.contains(keys[i]))
++hitCount;
}
WTF::compilerFence();
}
auto after = WTF::MonotonicTime::now();
if (hitCount != reps * n)
WTF::dataLog("WARNING: hit mismatch (", container, "/", keyType, "/n=", n, ")\n");
double totalNs = (after - before).nanoseconds();
g_rows.append({ container, "ContainsHit", keyType, n, totalNs / (static_cast<double>(reps) * n) });
}
template<template<typename...> class SetTemplate, typename T>
NEVER_INLINE void benchIterate(unsigned n, const char* container, const char* keyType)
{
if (!matchesFilter(container, "Iterate", keyType))
return;
auto keys = makeKeys<T>(n);
SetTemplate<T> set;
for (unsigned i = 0; i < n; ++i)
set.add(keys[i]);
unsigned reps = repsForIterate(n);
uintptr_t sink = 0;
auto before = WTF::MonotonicTime::now();
for (unsigned r = 0; r < reps; ++r) {
for (const auto& v : set) {
if constexpr (std::is_same_v<T, WTF::String>)
sink ^= v.impl() ? v.impl()->hash() : 0u;
else if constexpr (std::is_same_v<T, void*>)
sink ^= std::bit_cast<uintptr_t>(v);
else
sink ^= static_cast<uintptr_t>(v);
}
WTF::compilerFence();
}
auto after = WTF::MonotonicTime::now();
// Prevent dead-code elimination.
asm volatile("" : : "r"(sink) : "memory");
double totalNs = (after - before).nanoseconds();
g_rows.append({ container, "Iterate", keyType, n, totalNs / (static_cast<double>(reps) * n) });
}
// Churn: remove half the keys then re-insert them, repeated.
template<template<typename...> class SetTemplate, typename T>
NEVER_INLINE void benchChurn(unsigned n, const char* container, const char* keyType)
{
if (!matchesFilter(container, "Churn", keyType))
return;
auto keys = makeKeys<T>(n);
SetTemplate<T> set;
for (unsigned i = 0; i < n; ++i)
set.add(keys[i]);
unsigned half = n / 2;
if (!half)
half = 1;
unsigned reps = repsForChurn(n);
auto before = WTF::MonotonicTime::now();
for (unsigned r = 0; r < reps; ++r) {
// Remove the second half.
for (unsigned i = half; i < n; ++i)
set.remove(keys[i]);
// Reinsert them.
for (unsigned i = half; i < n; ++i)
set.add(keys[i]);
WTF::compilerFence();
}
auto after = WTF::MonotonicTime::now();
double totalNs = (after - before).nanoseconds();
// Each rep does (n - half) removes + (n - half) inserts. Normalize per-op.
double opsPerRep = 2.0 * static_cast<double>(n - half);
g_rows.append({ container, "Churn", keyType, n, totalNs / (static_cast<double>(reps) * opsPerRep) });
}
template<template<typename...> class SetTemplate, typename T>
void runAllOps(const char* container, const char* keyType)
{
for (unsigned n : kCapacities) {
benchInsert<SetTemplate, T>(n, container, keyType);
benchContainsHit<SetTemplate, T>(n, container, keyType);
benchIterate<SetTemplate, T>(n, container, keyType);
benchChurn<SetTemplate, T>(n, container, keyType);
}
}
template<typename T> using HashSetT = WTF::HashSet<T>;
template<typename T> using UncheckedKeyHashSetT = WTF::UncheckedKeyHashSet<T>;
template<typename T> using OrderedHashSetT = WTF::OrderedHashSet<T>;
template<typename T> using ListHashSetT = WTF::ListHashSet<T>;
void printRows()
{
WTF::dataLog("\n");
WTF::dataLog("container op key n ns/op\n");
WTF::dataLog("--------- -- --- - -----\n");
for (const auto& row : g_rows) {
char line[256];
snprintf(line, sizeof(line),
"%-16s %-11s %-8s %7u %10.2f\n",
row.container, row.op, row.keyType, row.n, row.nsPerOp);
WTF::dataLogF("%s", line);
}
}
const char* prettyN(unsigned n, char* buf, size_t bufSize)
{
if (n >= 1024 * 1024)
snprintf(buf, bufSize, "%uM", n / (1024 * 1024));
else if (n >= 1024)
snprintf(buf, bufSize, "%uK", n / 1024);
else
snprintf(buf, bufSize, "%u", n);
return buf;
}
const Row* findRow(const char* container, const char* op, const char* keyType, unsigned n)
{
for (const auto& row : g_rows) {
if (row.n == n
&& !strcmp(row.container, container)
&& !strcmp(row.op, op)
&& !strcmp(row.keyType, keyType))
return &row;
}
return nullptr;
}
void printMatrix()
{
static const char* kOps[] = { "Insert", "ContainsHit", "Iterate", "Churn" };
static const char* kContainers[] = { "HashSet", "UncheckedHashSet", "OrderedHashSet", "ListHashSet" };
static const char* kKeyTypes[] = { "uint32_t", "void*", "String" };
for (const char* op : kOps) {
WTF::dataLog("\n=== ", op, " (ns/op) ===\n");
// Header.
char line[512];
int written = snprintf(line, sizeof(line), "%-16s %-8s", "container", "key");
for (unsigned n : kCapacities) {
char nbuf[16];
written += snprintf(line + written, sizeof(line) - written, " %8s", prettyN(n, nbuf, sizeof(nbuf)));
}
snprintf(line + written, sizeof(line) - written, "\n");
WTF::dataLogF("%s", line);
// Separator.
WTF::dataLog("---------------- --------");
for (unsigned i = 0; i < std::size(kCapacities); ++i)
WTF::dataLog(" --------");
WTF::dataLog("\n");
// Rows: outer loop key type, inner loop container, so that each container
// group is contiguous within a key type.
for (const char* keyType : kKeyTypes) {
for (const char* container : kContainers) {
if (!matchesFilter(container, op, keyType))
continue;
written = snprintf(line, sizeof(line), "%-16s %-8s", container, keyType);
for (unsigned n : kCapacities) {
if (const Row* r = findRow(container, op, keyType, n))
written += snprintf(line + written, sizeof(line) - written, " %8.2f", r->nsPerOp);
else
written += snprintf(line + written, sizeof(line) - written, " %8s", "-");
}
snprintf(line + written, sizeof(line) - written, "\n");
WTF::dataLogF("%s", line);
}
}
}
}
} // anonymous namespace
int main(int argc, char** argv)
{
WTF::initialize();
if (argc >= 2)
g_filter = argv[1];
if (argc >= 3)
g_repMultiplier = atof(argv[2]);
WTF::dataLog("HashSetVariantsBenchmark — comparing WTF::HashSet, WTF::OrderedHashSet, WTF::ListHashSet\n");
WTF::dataLog("Capacities: ");
for (unsigned n : kCapacities)
WTF::dataLog(n, " ");
WTF::dataLog("\n");
if (g_filter)
WTF::dataLog("Filter: ", g_filter, "\n");
if (g_repMultiplier != 1.0)
WTF::dataLog("Rep multiplier: ", g_repMultiplier, "\n");
auto wallStart = WTF::MonotonicTime::now();
runAllOps<HashSetT, uint32_t>("HashSet", "uint32_t");
runAllOps<UncheckedKeyHashSetT, uint32_t>("UncheckedHashSet", "uint32_t");
runAllOps<OrderedHashSetT, uint32_t>("OrderedHashSet", "uint32_t");
runAllOps<ListHashSetT, uint32_t>("ListHashSet", "uint32_t");
runAllOps<HashSetT, void*>("HashSet", "void*");
runAllOps<UncheckedKeyHashSetT, void*>("UncheckedHashSet", "void*");
runAllOps<OrderedHashSetT, void*>("OrderedHashSet", "void*");
runAllOps<ListHashSetT, void*>("ListHashSet", "void*");
runAllOps<HashSetT, WTF::String>("HashSet", "String");
runAllOps<UncheckedKeyHashSetT, WTF::String>("UncheckedHashSet", "String");
runAllOps<OrderedHashSetT, WTF::String>("OrderedHashSet", "String");
runAllOps<ListHashSetT, WTF::String>("ListHashSet", "String");
auto wallEnd = WTF::MonotonicTime::now();
printRows();
printMatrix();
WTF::dataLog("\nTotal benchmark wall time: ", (wallEnd - wallStart).seconds(), " s\n");
return 0;
}
WTF_ALLOW_UNSAFE_BUFFER_USAGE_END