/* * fp-bench.c - A collection of simple floating point microbenchmarks. * * Copyright (C) 2018, Emilio G. Cota * * License: GNU GPL, version 2 or later. * See the COPYING file in the top-level directory. */ #ifndef HW_POISON_H #error Must define HW_POISON_H to work around TARGET_* poisoning #endif #include "qemu/osdep.h" #include #include #include "qemu/timer.h" #include "qemu/int128.h" #include "fpu/softfloat.h" /* amortize the computation of random inputs */ #define OPS_PER_ITER 50000 #define MAX_OPERANDS 3 #define SEED_A 0xdeadfacedeadface #define SEED_B 0xbadc0feebadc0fee #define SEED_C 0xbeefdeadbeefdead enum op { OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_FMA, OP_SQRT, OP_CMP, OP_MAX_NR, }; static const char * const op_names[] = { [OP_ADD] = "add", [OP_SUB] = "sub", [OP_MUL] = "mul", [OP_DIV] = "div", [OP_FMA] = "mulAdd", [OP_SQRT] = "sqrt", [OP_CMP] = "cmp", [OP_MAX_NR] = NULL, }; enum precision { PREC_SINGLE, PREC_DOUBLE, PREC_QUAD, PREC_FLOAT32, PREC_FLOAT64, PREC_FLOAT128, PREC_MAX_NR, }; enum rounding { ROUND_EVEN, ROUND_ZERO, ROUND_DOWN, ROUND_UP, ROUND_TIEAWAY, N_ROUND_MODES, }; static const char * const round_names[] = { [ROUND_EVEN] = "even", [ROUND_ZERO] = "zero", [ROUND_DOWN] = "down", [ROUND_UP] = "up", [ROUND_TIEAWAY] = "tieaway", }; enum tester { TESTER_SOFT, TESTER_HOST, TESTER_MAX_NR, }; static const char * const tester_names[] = { [TESTER_SOFT] = "soft", [TESTER_HOST] = "host", [TESTER_MAX_NR] = NULL, }; union fp { float f; double d; float32 f32; float64 f64; float128 f128; uint64_t u64; }; struct op_state; typedef float (*float_func_t)(const struct op_state *s); typedef double (*double_func_t)(const struct op_state *s); union fp_func { float_func_t float_func; double_func_t double_func; }; typedef void (*bench_func_t)(void); struct op_desc { const char * const name; }; #define DEFAULT_DURATION_SECS 1 static uint64_t random_ops[MAX_OPERANDS] = { SEED_A, SEED_B, SEED_C, }; static float128 random_quad_ops[MAX_OPERANDS] = { {SEED_A, SEED_B}, {SEED_B, SEED_C}, {SEED_C, SEED_A}, }; static float_status soft_status; static enum precision precision; static enum op operation; static enum tester tester; static uint64_t n_completed_ops; static unsigned int duration = DEFAULT_DURATION_SECS; static int64_t ns_elapsed; /* disable optimizations with volatile */ static volatile union fp res; /* * 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 update_random_ops(int n_ops, enum precision prec) { int i; for (i = 0; i < n_ops; i++) { switch (prec) { case PREC_SINGLE: case PREC_FLOAT32: { uint64_t r = random_ops[i]; do { r = xorshift64star(r); } while (!float32_is_normal(r)); random_ops[i] = r; break; } case PREC_DOUBLE: case PREC_FLOAT64: { uint64_t r = random_ops[i]; do { r = xorshift64star(r); } while (!float64_is_normal(r)); random_ops[i] = r; break; } case PREC_QUAD: case PREC_FLOAT128: { float128 r = random_quad_ops[i]; uint64_t hi = r.high; uint64_t lo = r.low; do { hi = xorshift64star(hi); lo = xorshift64star(lo); r = make_float128(hi, lo); } while (!float128_is_normal(r)); random_quad_ops[i] = r; break; } default: g_assert_not_reached(); } } } static void fill_random(union fp *ops, int n_ops, enum precision prec, bool no_neg) { int i; for (i = 0; i < n_ops; i++) { switch (prec) { case PREC_SINGLE: case PREC_FLOAT32: ops[i].f32 = make_float32(random_ops[i]); if (no_neg && float32_is_neg(ops[i].f32)) { ops[i].f32 = float32_chs(ops[i].f32); } break; case PREC_DOUBLE: case PREC_FLOAT64: ops[i].f64 = make_float64(random_ops[i]); if (no_neg && float64_is_neg(ops[i].f64)) { ops[i].f64 = float64_chs(ops[i].f64); } break; case PREC_QUAD: case PREC_FLOAT128: ops[i].f128 = random_quad_ops[i]; if (no_neg && float128_is_neg(ops[i].f128)) { ops[i].f128 = float128_chs(ops[i].f128); } break; default: g_assert_not_reached(); } } } /* * The main benchmark function. Instead of (ab)using macros, we rely * on the compiler to unfold this at compile-time. */ static void bench(enum precision prec, enum op op, int n_ops, bool no_neg) { int64_t tf = get_clock() + duration * 1000000000LL; while (get_clock() < tf) { union fp ops[MAX_OPERANDS]; int64_t t0; int i; update_random_ops(n_ops, prec); switch (prec) { case PREC_SINGLE: fill_random(ops, n_ops, prec, no_neg); t0 = get_clock(); for (i = 0; i < OPS_PER_ITER; i++) { float a = ops[0].f; float b = ops[1].f; float c = ops[2].f; switch (op) { case OP_ADD: res.f = a + b; break; case OP_SUB: res.f = a - b; break; case OP_MUL: res.f = a * b; break; case OP_DIV: res.f = a / b; break; case OP_FMA: res.f = fmaf(a, b, c); break; case OP_SQRT: res.f = sqrtf(a); break; case OP_CMP: res.u64 = isgreater(a, b); break; default: g_assert_not_reached(); } } break; case PREC_DOUBLE: fill_random(ops, n_ops, prec, no_neg); t0 = get_clock(); for (i = 0; i < OPS_PER_ITER; i++) { double a = ops[0].d; double b = ops[1].d; double c = ops[2].d; switch (op) { case OP_ADD: res.d = a + b; break; case OP_SUB: res.d = a - b; break; case OP_MUL: res.d = a * b; break; case OP_DIV: res.d = a / b; break; case OP_FMA: res.d = fma(a, b, c); break; case OP_SQRT: res.d = sqrt(a); break; case OP_CMP: res.u64 = isgreater(a, b); break; default: g_assert_not_reached(); } } break; case PREC_FLOAT32: fill_random(ops, n_ops, prec, no_neg); t0 = get_clock(); for (i = 0; i < OPS_PER_ITER; i++) { float32 a = ops[0].f32; float32 b = ops[1].f32; float32 c = ops[2].f32; switch (op) { case OP_ADD: res.f32 = float32_add(a, b, &soft_status); break; case OP_SUB: res.f32 = float32_sub(a, b, &soft_status); break; case OP_MUL: res.f = float32_mul(a, b, &soft_status); break; case OP_DIV: res.f32 = float32_div(a, b, &soft_status); break; case OP_FMA: res.f32 = float32_muladd(a, b, c, 0, &soft_status); break; case OP_SQRT: res.f32 = float32_sqrt(a, &soft_status); break; case OP_CMP: res.u64 = float32_compare_quiet(a, b, &soft_status); break; default: g_assert_not_reached(); } } break; case PREC_FLOAT64: fill_random(ops, n_ops, prec, no_neg); t0 = get_clock(); for (i = 0; i < OPS_PER_ITER; i++) { float64 a = ops[0].f64; float64 b = ops[1].f64; float64 c = ops[2].f64; switch (op) { case OP_ADD: res.f64 = float64_add(a, b, &soft_status); break; case OP_SUB: res.f64 = float64_sub(a, b, &soft_status); break; case OP_MUL: res.f = float64_mul(a, b, &soft_status); break; case OP_DIV: res.f64 = float64_div(a, b, &soft_status); break; case OP_FMA: res.f64 = float64_muladd(a, b, c, 0, &soft_status); break; case OP_SQRT: res.f64 = float64_sqrt(a, &soft_status); break; case OP_CMP: res.u64 = float64_compare_quiet(a, b, &soft_status); break; default: g_assert_not_reached(); } } break; case PREC_FLOAT128: fill_random(ops, n_ops, prec, no_neg); t0 = get_clock(); for (i = 0; i < OPS_PER_ITER; i++) { float128 a = ops[0].f128; float128 b = ops[1].f128; float128 c = ops[2].f128; switch (op) { case OP_ADD: res.f128 = float128_add(a, b, &soft_status); break; case OP_SUB: res.f128 = float128_sub(a, b, &soft_status); break; case OP_MUL: res.f128 = float128_mul(a, b, &soft_status); break; case OP_DIV: res.f128 = float128_div(a, b, &soft_status); break; case OP_FMA: res.f128 = float128_muladd(a, b, c, 0, &soft_status); break; case OP_SQRT: res.f128 = float128_sqrt(a, &soft_status); break; case OP_CMP: res.u64 = float128_compare_quiet(a, b, &soft_status); break; default: g_assert_not_reached(); } } break; default: g_assert_not_reached(); } ns_elapsed += get_clock() - t0; n_completed_ops += OPS_PER_ITER; } } #define GEN_BENCH(name, type, prec, op, n_ops) \ static void __attribute__((flatten)) name(void) \ { \ bench(prec, op, n_ops, false); \ } #define GEN_BENCH_NO_NEG(name, type, prec, op, n_ops) \ static void __attribute__((flatten)) name(void) \ { \ bench(prec, op, n_ops, true); \ } #define GEN_BENCH_ALL_TYPES(opname, op, n_ops) \ GEN_BENCH(bench_ ## opname ## _float, float, PREC_SINGLE, op, n_ops) \ GEN_BENCH(bench_ ## opname ## _double, double, PREC_DOUBLE, op, n_ops) \ GEN_BENCH(bench_ ## opname ## _float32, float32, PREC_FLOAT32, op, n_ops) \ GEN_BENCH(bench_ ## opname ## _float64, float64, PREC_FLOAT64, op, n_ops) \ GEN_BENCH(bench_ ## opname ## _float128, float128, PREC_FLOAT128, op, n_ops) GEN_BENCH_ALL_TYPES(add, OP_ADD, 2) GEN_BENCH_ALL_TYPES(sub, OP_SUB, 2) GEN_BENCH_ALL_TYPES(mul, OP_MUL, 2) GEN_BENCH_ALL_TYPES(div, OP_DIV, 2) GEN_BENCH_ALL_TYPES(fma, OP_FMA, 3) GEN_BENCH_ALL_TYPES(cmp, OP_CMP, 2) #undef GEN_BENCH_ALL_TYPES #define GEN_BENCH_ALL_TYPES_NO_NEG(name, op, n) \ GEN_BENCH_NO_NEG(bench_ ## name ## _float, float, PREC_SINGLE, op, n) \ GEN_BENCH_NO_NEG(bench_ ## name ## _double, double, PREC_DOUBLE, op, n) \ GEN_BENCH_NO_NEG(bench_ ## name ## _float32, float32, PREC_FLOAT32, op, n) \ GEN_BENCH_NO_NEG(bench_ ## name ## _float64, float64, PREC_FLOAT64, op, n) \ GEN_BENCH_NO_NEG(bench_ ## name ## _float128, float128, PREC_FLOAT128, op, n) GEN_BENCH_ALL_TYPES_NO_NEG(sqrt, OP_SQRT, 1) #undef GEN_BENCH_ALL_TYPES_NO_NEG #undef GEN_BENCH_NO_NEG #undef GEN_BENCH #define GEN_BENCH_FUNCS(opname, op) \ [op] = { \ [PREC_SINGLE] = bench_ ## opname ## _float, \ [PREC_DOUBLE] = bench_ ## opname ## _double, \ [PREC_FLOAT32] = bench_ ## opname ## _float32, \ [PREC_FLOAT64] = bench_ ## opname ## _float64, \ [PREC_FLOAT128] = bench_ ## opname ## _float128, \ } static const bench_func_t bench_funcs[OP_MAX_NR][PREC_MAX_NR] = { GEN_BENCH_FUNCS(add, OP_ADD), GEN_BENCH_FUNCS(sub, OP_SUB), GEN_BENCH_FUNCS(mul, OP_MUL), GEN_BENCH_FUNCS(div, OP_DIV), GEN_BENCH_FUNCS(fma, OP_FMA), GEN_BENCH_FUNCS(sqrt, OP_SQRT), GEN_BENCH_FUNCS(cmp, OP_CMP), }; #undef GEN_BENCH_FUNCS static void run_bench(void) { bench_func_t f; set_float_2nan_prop_rule(float_2nan_prop_s_ab, &soft_status); f = bench_funcs[operation][precision]; g_assert(f); f(); } /* @arr must be NULL-terminated */ static int find_name(const char * const *arr, const char *name) { int i; for (i = 0; arr[i] != NULL; i++) { if (strcmp(name, arr[i]) == 0) { return i; } } return -1; } static void usage_complete(int argc, char *argv[]) { gchar *op_list = g_strjoinv(", ", (gchar **)op_names); gchar *tester_list = g_strjoinv(", ", (gchar **)tester_names); fprintf(stderr, "Usage: %s [options]\n", argv[0]); fprintf(stderr, "options:\n"); fprintf(stderr, " -d = duration, in seconds. Default: %d\n", DEFAULT_DURATION_SECS); fprintf(stderr, " -h = show this help message.\n"); fprintf(stderr, " -o = floating point operation (%s). Default: %s\n", op_list, op_names[0]); fprintf(stderr, " -p = floating point precision (single, double, quad[soft only]). " "Default: single\n"); fprintf(stderr, " -r = rounding mode (even, zero, down, up, tieaway). " "Default: even\n"); fprintf(stderr, " -t = tester (%s). Default: %s\n", tester_list, tester_names[0]); fprintf(stderr, " -z = flush inputs to zero (soft tester only). " "Default: disabled\n"); fprintf(stderr, " -Z = flush output to zero (soft tester only). " "Default: disabled\n"); g_free(tester_list); g_free(op_list); } static int round_name_to_mode(const char *name) { int i; for (i = 0; i < N_ROUND_MODES; i++) { if (!strcmp(round_names[i], name)) { return i; } } return -1; } static G_NORETURN void die_host_rounding(enum rounding rounding) { fprintf(stderr, "fatal: '%s' rounding not supported on this host\n", round_names[rounding]); exit(EXIT_FAILURE); } static void set_host_precision(enum rounding rounding) { int rhost; switch (rounding) { case ROUND_EVEN: rhost = FE_TONEAREST; break; case ROUND_ZERO: rhost = FE_TOWARDZERO; break; case ROUND_DOWN: rhost = FE_DOWNWARD; break; case ROUND_UP: rhost = FE_UPWARD; break; case ROUND_TIEAWAY: die_host_rounding(rounding); return; default: g_assert_not_reached(); } if (fesetround(rhost)) { die_host_rounding(rounding); } } static void set_soft_precision(enum rounding rounding) { signed char mode; switch (rounding) { case ROUND_EVEN: mode = float_round_nearest_even; break; case ROUND_ZERO: mode = float_round_to_zero; break; case ROUND_DOWN: mode = float_round_down; break; case ROUND_UP: mode = float_round_up; break; case ROUND_TIEAWAY: mode = float_round_ties_away; break; default: g_assert_not_reached(); } soft_status.float_rounding_mode = mode; } static void parse_args(int argc, char *argv[]) { int c; int val; int rounding = ROUND_EVEN; for (;;) { c = getopt(argc, argv, "d:ho:p:r:t:zZ"); if (c < 0) { break; } switch (c) { case 'd': duration = atoi(optarg); break; case 'h': usage_complete(argc, argv); exit(EXIT_SUCCESS); case 'o': val = find_name(op_names, optarg); if (val < 0) { fprintf(stderr, "Unsupported op '%s'\n", optarg); exit(EXIT_FAILURE); } operation = val; break; case 'p': if (!strcmp(optarg, "single")) { precision = PREC_SINGLE; } else if (!strcmp(optarg, "double")) { precision = PREC_DOUBLE; } else if (!strcmp(optarg, "quad")) { precision = PREC_QUAD; } else { fprintf(stderr, "Unsupported precision '%s'\n", optarg); exit(EXIT_FAILURE); } break; case 'r': rounding = round_name_to_mode(optarg); if (rounding < 0) { fprintf(stderr, "fatal: invalid rounding mode '%s'\n", optarg); exit(EXIT_FAILURE); } break; case 't': val = find_name(tester_names, optarg); if (val < 0) { fprintf(stderr, "Unsupported tester '%s'\n", optarg); exit(EXIT_FAILURE); } tester = val; break; case 'z': soft_status.flush_inputs_to_zero = 1; break; case 'Z': soft_status.flush_to_zero = 1; break; } } /* set precision and rounding mode based on the tester */ switch (tester) { case TESTER_HOST: set_host_precision(rounding); break; case TESTER_SOFT: set_soft_precision(rounding); switch (precision) { case PREC_SINGLE: precision = PREC_FLOAT32; break; case PREC_DOUBLE: precision = PREC_FLOAT64; break; case PREC_QUAD: precision = PREC_FLOAT128; break; default: g_assert_not_reached(); } break; default: g_assert_not_reached(); } } static void pr_stats(void) { printf("%.2f MFlops\n", (double)n_completed_ops / ns_elapsed * 1e3); } int main(int argc, char *argv[]) { parse_args(argc, argv); run_bench(); pr_stats(); return 0; }