Precomputing the hash values allows us to perform more frequent accesses to the hash table, thereby reaching higher throughputs. We keep the old behaviour by default, since (1) we might confuse users if they measured a speedup without changing anything in the QHT implementation, and (2) benchmarking the hash function "on line" is also valuable. Before: $ taskset -c 0 tests/qht-bench -n 1 Throughput: 38.18 MT/s After: $ taskset -c 0 tests/qht-bench -n 1 Throughput: 38.16 MT/s After (with precomputing): $ taskset -c 0 tests/qht-bench -n 1 -p Throughput: 50.87 MT/s Signed-off-by: Emilio G. Cota <cota@braap.org> Signed-off-by: Richard Henderson <richard.henderson@linaro.org>
502 lines
13 KiB
C
502 lines
13 KiB
C
/*
|
|
* Copyright (C) 2016, Emilio G. Cota <cota@braap.org>
|
|
*
|
|
* License: GNU GPL, version 2 or later.
|
|
* See the COPYING file in the top-level directory.
|
|
*/
|
|
#include "qemu/osdep.h"
|
|
#include "qemu/processor.h"
|
|
#include "qemu/atomic.h"
|
|
#include "qemu/qht.h"
|
|
#include "qemu/rcu.h"
|
|
#include "exec/tb-hash-xx.h"
|
|
|
|
struct thread_stats {
|
|
size_t rd;
|
|
size_t not_rd;
|
|
size_t in;
|
|
size_t not_in;
|
|
size_t rm;
|
|
size_t not_rm;
|
|
size_t rz;
|
|
size_t not_rz;
|
|
};
|
|
|
|
struct thread_info {
|
|
void (*func)(struct thread_info *);
|
|
struct thread_stats stats;
|
|
uint64_t r;
|
|
bool write_op; /* writes alternate between insertions and removals */
|
|
bool resize_down;
|
|
} QEMU_ALIGNED(64); /* avoid false sharing among threads */
|
|
|
|
static struct qht ht;
|
|
static QemuThread *rw_threads;
|
|
|
|
#define DEFAULT_RANGE (4096)
|
|
#define DEFAULT_QHT_N_ELEMS DEFAULT_RANGE
|
|
|
|
static unsigned int duration = 1;
|
|
static unsigned int n_rw_threads = 1;
|
|
static unsigned long lookup_range = DEFAULT_RANGE;
|
|
static unsigned long update_range = DEFAULT_RANGE;
|
|
static size_t init_range = DEFAULT_RANGE;
|
|
static size_t init_size = DEFAULT_RANGE;
|
|
static size_t n_ready_threads;
|
|
static long populate_offset;
|
|
static long *keys;
|
|
|
|
static size_t resize_min;
|
|
static size_t resize_max;
|
|
static struct thread_info *rz_info;
|
|
static unsigned long resize_delay = 1000;
|
|
static double resize_rate; /* 0.0 to 1.0 */
|
|
static unsigned int n_rz_threads = 1;
|
|
static QemuThread *rz_threads;
|
|
static bool precompute_hash;
|
|
|
|
static double update_rate; /* 0.0 to 1.0 */
|
|
static uint64_t update_threshold;
|
|
static uint64_t resize_threshold;
|
|
|
|
static size_t qht_n_elems = DEFAULT_QHT_N_ELEMS;
|
|
static int qht_mode;
|
|
|
|
static bool test_start;
|
|
static bool test_stop;
|
|
|
|
static struct thread_info *rw_info;
|
|
|
|
static const char commands_string[] =
|
|
" -d = duration, in seconds\n"
|
|
" -n = number of threads\n"
|
|
"\n"
|
|
" -o = offset at which keys start\n"
|
|
"\n"
|
|
" -g = set -s,-k,-K,-l,-r to the same value\n"
|
|
" -s = initial size hint\n"
|
|
" -k = initial number of keys\n"
|
|
" -K = initial range of keys (will be rounded up to pow2)\n"
|
|
" -l = lookup range of keys (will be rounded up to pow2)\n"
|
|
" -r = update range of keys (will be rounded up to pow2)\n"
|
|
"\n"
|
|
" -u = update rate (0.0 to 100.0), 50/50 split of insertions/removals\n"
|
|
"\n"
|
|
" -R = enable auto-resize\n"
|
|
" -S = resize rate (0.0 to 100.0)\n"
|
|
" -D = delay (in us) between potential resizes\n"
|
|
" -N = number of resize threads";
|
|
|
|
static void usage_complete(int argc, char *argv[])
|
|
{
|
|
fprintf(stderr, "Usage: %s [options]\n", argv[0]);
|
|
fprintf(stderr, "options:\n%s\n", commands_string);
|
|
exit(-1);
|
|
}
|
|
|
|
static bool is_equal(const void *ap, const void *bp)
|
|
{
|
|
const long *a = ap;
|
|
const long *b = bp;
|
|
|
|
return *a == *b;
|
|
}
|
|
|
|
static uint32_t h(unsigned long v)
|
|
{
|
|
return tb_hash_func7(v, 0, 0, 0, 0);
|
|
}
|
|
|
|
static uint32_t hval(unsigned long v)
|
|
{
|
|
return v;
|
|
}
|
|
|
|
static uint32_t (*hfunc)(unsigned long v) = h;
|
|
|
|
/*
|
|
* From: https://en.wikipedia.org/wiki/Xorshift
|
|
* This is faster than rand_r(), and gives us a wider range (RAND_MAX is only
|
|
* guaranteed to be >= INT_MAX).
|
|
*/
|
|
static uint64_t xorshift64star(uint64_t x)
|
|
{
|
|
x ^= x >> 12; /* a */
|
|
x ^= x << 25; /* b */
|
|
x ^= x >> 27; /* c */
|
|
return x * UINT64_C(2685821657736338717);
|
|
}
|
|
|
|
static void do_rz(struct thread_info *info)
|
|
{
|
|
struct thread_stats *stats = &info->stats;
|
|
|
|
if (info->r < resize_threshold) {
|
|
size_t size = info->resize_down ? resize_min : resize_max;
|
|
bool resized;
|
|
|
|
resized = qht_resize(&ht, size);
|
|
info->resize_down = !info->resize_down;
|
|
|
|
if (resized) {
|
|
stats->rz++;
|
|
} else {
|
|
stats->not_rz++;
|
|
}
|
|
}
|
|
g_usleep(resize_delay);
|
|
}
|
|
|
|
static void do_rw(struct thread_info *info)
|
|
{
|
|
struct thread_stats *stats = &info->stats;
|
|
uint32_t hash;
|
|
long *p;
|
|
|
|
if (info->r >= update_threshold) {
|
|
bool read;
|
|
|
|
p = &keys[info->r & (lookup_range - 1)];
|
|
hash = hfunc(*p);
|
|
read = qht_lookup(&ht, p, hash);
|
|
if (read) {
|
|
stats->rd++;
|
|
} else {
|
|
stats->not_rd++;
|
|
}
|
|
} else {
|
|
p = &keys[info->r & (update_range - 1)];
|
|
hash = hfunc(*p);
|
|
if (info->write_op) {
|
|
bool written = false;
|
|
|
|
if (qht_lookup(&ht, p, hash) == NULL) {
|
|
written = qht_insert(&ht, p, hash, NULL);
|
|
}
|
|
if (written) {
|
|
stats->in++;
|
|
} else {
|
|
stats->not_in++;
|
|
}
|
|
} else {
|
|
bool removed = false;
|
|
|
|
if (qht_lookup(&ht, p, hash)) {
|
|
removed = qht_remove(&ht, p, hash);
|
|
}
|
|
if (removed) {
|
|
stats->rm++;
|
|
} else {
|
|
stats->not_rm++;
|
|
}
|
|
}
|
|
info->write_op = !info->write_op;
|
|
}
|
|
}
|
|
|
|
static void *thread_func(void *p)
|
|
{
|
|
struct thread_info *info = p;
|
|
|
|
rcu_register_thread();
|
|
|
|
atomic_inc(&n_ready_threads);
|
|
while (!atomic_read(&test_start)) {
|
|
cpu_relax();
|
|
}
|
|
|
|
rcu_read_lock();
|
|
while (!atomic_read(&test_stop)) {
|
|
info->r = xorshift64star(info->r);
|
|
info->func(info);
|
|
}
|
|
rcu_read_unlock();
|
|
|
|
rcu_unregister_thread();
|
|
return NULL;
|
|
}
|
|
|
|
/* sets everything except info->func */
|
|
static void prepare_thread_info(struct thread_info *info, int i)
|
|
{
|
|
/* seed for the RNG; each thread should have a different one */
|
|
info->r = (i + 1) ^ time(NULL);
|
|
/* the first update will be a write */
|
|
info->write_op = true;
|
|
/* the first resize will be down */
|
|
info->resize_down = true;
|
|
|
|
memset(&info->stats, 0, sizeof(info->stats));
|
|
}
|
|
|
|
static void
|
|
th_create_n(QemuThread **threads, struct thread_info **infos, const char *name,
|
|
void (*func)(struct thread_info *), int offset, int n)
|
|
{
|
|
struct thread_info *info;
|
|
QemuThread *th;
|
|
int i;
|
|
|
|
th = g_malloc(sizeof(*th) * n);
|
|
*threads = th;
|
|
|
|
info = qemu_memalign(64, sizeof(*info) * n);
|
|
*infos = info;
|
|
|
|
for (i = 0; i < n; i++) {
|
|
prepare_thread_info(&info[i], offset + i);
|
|
info[i].func = func;
|
|
qemu_thread_create(&th[i], name, thread_func, &info[i],
|
|
QEMU_THREAD_JOINABLE);
|
|
}
|
|
}
|
|
|
|
static void create_threads(void)
|
|
{
|
|
th_create_n(&rw_threads, &rw_info, "rw", do_rw, 0, n_rw_threads);
|
|
th_create_n(&rz_threads, &rz_info, "rz", do_rz, n_rw_threads, n_rz_threads);
|
|
}
|
|
|
|
static void pr_params(void)
|
|
{
|
|
printf("Parameters:\n");
|
|
printf(" duration: %d s\n", duration);
|
|
printf(" # of threads: %u\n", n_rw_threads);
|
|
printf(" initial # of keys: %zu\n", init_size);
|
|
printf(" initial size hint: %zu\n", qht_n_elems);
|
|
printf(" auto-resize: %s\n",
|
|
qht_mode & QHT_MODE_AUTO_RESIZE ? "on" : "off");
|
|
if (resize_rate) {
|
|
printf(" resize_rate: %f%%\n", resize_rate * 100.0);
|
|
printf(" resize range: %zu-%zu\n", resize_min, resize_max);
|
|
printf(" # resize threads %u\n", n_rz_threads);
|
|
}
|
|
printf(" update rate: %f%%\n", update_rate * 100.0);
|
|
printf(" offset: %ld\n", populate_offset);
|
|
printf(" initial key range: %zu\n", init_range);
|
|
printf(" lookup range: %lu\n", lookup_range);
|
|
printf(" update range: %lu\n", update_range);
|
|
}
|
|
|
|
static void do_threshold(double rate, uint64_t *threshold)
|
|
{
|
|
if (rate == 1.0) {
|
|
*threshold = UINT64_MAX;
|
|
} else {
|
|
*threshold = rate * UINT64_MAX;
|
|
}
|
|
}
|
|
|
|
static void htable_init(void)
|
|
{
|
|
unsigned long n = MAX(init_range, update_range);
|
|
uint64_t r = time(NULL);
|
|
size_t retries = 0;
|
|
size_t i;
|
|
|
|
/* avoid allocating memory later by allocating all the keys now */
|
|
keys = g_malloc(sizeof(*keys) * n);
|
|
for (i = 0; i < n; i++) {
|
|
long val = populate_offset + i;
|
|
|
|
keys[i] = precompute_hash ? h(val) : hval(val);
|
|
}
|
|
|
|
/* some sanity checks */
|
|
g_assert_cmpuint(lookup_range, <=, n);
|
|
|
|
/* compute thresholds */
|
|
do_threshold(update_rate, &update_threshold);
|
|
do_threshold(resize_rate, &resize_threshold);
|
|
|
|
if (resize_rate) {
|
|
resize_min = n / 2;
|
|
resize_max = n;
|
|
assert(resize_min < resize_max);
|
|
} else {
|
|
n_rz_threads = 0;
|
|
}
|
|
|
|
/* initialize the hash table */
|
|
qht_init(&ht, is_equal, qht_n_elems, qht_mode);
|
|
assert(init_size <= init_range);
|
|
|
|
pr_params();
|
|
|
|
fprintf(stderr, "Initialization: populating %zu items...", init_size);
|
|
for (i = 0; i < init_size; i++) {
|
|
for (;;) {
|
|
uint32_t hash;
|
|
long *p;
|
|
|
|
r = xorshift64star(r);
|
|
p = &keys[r & (init_range - 1)];
|
|
hash = hfunc(*p);
|
|
if (qht_insert(&ht, p, hash, NULL)) {
|
|
break;
|
|
}
|
|
retries++;
|
|
}
|
|
}
|
|
fprintf(stderr, " populated after %zu retries\n", retries);
|
|
}
|
|
|
|
static void add_stats(struct thread_stats *s, struct thread_info *info, int n)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < n; i++) {
|
|
struct thread_stats *stats = &info[i].stats;
|
|
|
|
s->rd += stats->rd;
|
|
s->not_rd += stats->not_rd;
|
|
|
|
s->in += stats->in;
|
|
s->not_in += stats->not_in;
|
|
|
|
s->rm += stats->rm;
|
|
s->not_rm += stats->not_rm;
|
|
|
|
s->rz += stats->rz;
|
|
s->not_rz += stats->not_rz;
|
|
}
|
|
}
|
|
|
|
static void pr_stats(void)
|
|
{
|
|
struct thread_stats s = {};
|
|
double tx;
|
|
|
|
add_stats(&s, rw_info, n_rw_threads);
|
|
add_stats(&s, rz_info, n_rz_threads);
|
|
|
|
printf("Results:\n");
|
|
|
|
if (resize_rate) {
|
|
printf(" Resizes: %zu (%.2f%% of %zu)\n",
|
|
s.rz, (double)s.rz / (s.rz + s.not_rz) * 100, s.rz + s.not_rz);
|
|
}
|
|
|
|
printf(" Read: %.2f M (%.2f%% of %.2fM)\n",
|
|
(double)s.rd / 1e6,
|
|
(double)s.rd / (s.rd + s.not_rd) * 100,
|
|
(double)(s.rd + s.not_rd) / 1e6);
|
|
printf(" Inserted: %.2f M (%.2f%% of %.2fM)\n",
|
|
(double)s.in / 1e6,
|
|
(double)s.in / (s.in + s.not_in) * 100,
|
|
(double)(s.in + s.not_in) / 1e6);
|
|
printf(" Removed: %.2f M (%.2f%% of %.2fM)\n",
|
|
(double)s.rm / 1e6,
|
|
(double)s.rm / (s.rm + s.not_rm) * 100,
|
|
(double)(s.rm + s.not_rm) / 1e6);
|
|
|
|
tx = (s.rd + s.not_rd + s.in + s.not_in + s.rm + s.not_rm) / 1e6 / duration;
|
|
printf(" Throughput: %.2f MT/s\n", tx);
|
|
printf(" Throughput/thread: %.2f MT/s/thread\n", tx / n_rw_threads);
|
|
}
|
|
|
|
static void run_test(void)
|
|
{
|
|
unsigned int remaining;
|
|
int i;
|
|
|
|
while (atomic_read(&n_ready_threads) != n_rw_threads + n_rz_threads) {
|
|
cpu_relax();
|
|
}
|
|
atomic_set(&test_start, true);
|
|
do {
|
|
remaining = sleep(duration);
|
|
} while (remaining);
|
|
atomic_set(&test_stop, true);
|
|
|
|
for (i = 0; i < n_rw_threads; i++) {
|
|
qemu_thread_join(&rw_threads[i]);
|
|
}
|
|
for (i = 0; i < n_rz_threads; i++) {
|
|
qemu_thread_join(&rz_threads[i]);
|
|
}
|
|
}
|
|
|
|
static void parse_args(int argc, char *argv[])
|
|
{
|
|
int c;
|
|
|
|
for (;;) {
|
|
c = getopt(argc, argv, "d:D:g:k:K:l:hn:N:o:pr:Rs:S:u:");
|
|
if (c < 0) {
|
|
break;
|
|
}
|
|
switch (c) {
|
|
case 'd':
|
|
duration = atoi(optarg);
|
|
break;
|
|
case 'D':
|
|
resize_delay = atol(optarg);
|
|
break;
|
|
case 'g':
|
|
init_range = pow2ceil(atol(optarg));
|
|
lookup_range = pow2ceil(atol(optarg));
|
|
update_range = pow2ceil(atol(optarg));
|
|
qht_n_elems = atol(optarg);
|
|
init_size = atol(optarg);
|
|
break;
|
|
case 'h':
|
|
usage_complete(argc, argv);
|
|
exit(0);
|
|
case 'k':
|
|
init_size = atol(optarg);
|
|
break;
|
|
case 'K':
|
|
init_range = pow2ceil(atol(optarg));
|
|
break;
|
|
case 'l':
|
|
lookup_range = pow2ceil(atol(optarg));
|
|
break;
|
|
case 'n':
|
|
n_rw_threads = atoi(optarg);
|
|
break;
|
|
case 'N':
|
|
n_rz_threads = atoi(optarg);
|
|
break;
|
|
case 'o':
|
|
populate_offset = atol(optarg);
|
|
break;
|
|
case 'p':
|
|
precompute_hash = true;
|
|
hfunc = hval;
|
|
break;
|
|
case 'r':
|
|
update_range = pow2ceil(atol(optarg));
|
|
break;
|
|
case 'R':
|
|
qht_mode |= QHT_MODE_AUTO_RESIZE;
|
|
break;
|
|
case 's':
|
|
qht_n_elems = atol(optarg);
|
|
break;
|
|
case 'S':
|
|
resize_rate = atof(optarg) / 100.0;
|
|
if (resize_rate > 1.0) {
|
|
resize_rate = 1.0;
|
|
}
|
|
break;
|
|
case 'u':
|
|
update_rate = atof(optarg) / 100.0;
|
|
if (update_rate > 1.0) {
|
|
update_rate = 1.0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
parse_args(argc, argv);
|
|
htable_init();
|
|
create_threads();
|
|
run_test();
|
|
pr_stats();
|
|
return 0;
|
|
}
|