diff --git a/.gitignore b/.gitignore index 9d3cdcf..b97e859 100644 --- a/.gitignore +++ b/.gitignore @@ -105,4 +105,5 @@ Makefile.in # Built benchmark harnesses (benchmarks/bench_* are generated from bench_*.c). /benchmarks/bench_vlibc +/benchmarks/bench_string diff --git a/benchmarks/Makefile.am b/benchmarks/Makefile.am index 552e8f5..58d1827 100644 --- a/benchmarks/Makefile.am +++ b/benchmarks/Makefile.am @@ -5,23 +5,49 @@ AM_CFLAGS = @VLIBC_CFLAGS@ # Built only on demand (via `make bench`), so `make all` does not need the # library to be built first. -EXTRA_PROGRAMS = bench_vlibc +EXTRA_PROGRAMS = bench_vlibc bench_string bench_vlibc_SOURCES = bench_vlibc.c +bench_string_SOURCES = bench_string.c + +# bench_string is a system-headers-only translation unit (it times whichever +# libc it is linked against), so it must NOT inherit AM_CPPFLAGS: -I include +# would pull vlibc's self-contained / into the same TU as +# the system / and double-define size_t/NULL/offsetof. +# bench_vlibc keeps the inherited -I include. +bench_string_CPPFLAGS = # By default benchmarks link against vlibc itself. --with-libc=glibc links the # same harness against the system glibc; --with-libc=musl builds it with # musl-gcc for the musl reference. if BENCH_LINK_VLIBC bench_vlibc_LDADD = ../libvlibc.la +bench_string_LDADD = ../libvlibc.la else bench_vlibc_LDADD = +bench_string_LDADD = endif if BENCH_LINK_MUSL CC = $(MUSL_CC) endif -bench: bench_vlibc +# Relink whenever the configuration changes (autogen.sh reruns configure and +# rewrites config.status). --with-libc only affects the link (LDADD), and the +# objects are compile-time identical, so make would otherwise reuse a stale +# binary from the previous configuration and the wrong libc would be measured. +EXTRA_bench_string_DEPENDENCIES = $(top_builddir)/config.status +EXTRA_bench_vlibc_DEPENDENCIES = $(top_builddir)/config.status + +# bench_vlibc.c calls vlibc_version(), which exists only in vlibc — under +# --with-libc=glibc/musl it cannot link, so it is built and run only in the +# vlibc build. bench_string links either way and runs always. +if BENCH_LINK_VLIBC +bench: bench_vlibc bench_string ./bench_vlibc + ./bench_string +else +bench: bench_string + ./bench_string +endif .PHONY: bench diff --git a/benchmarks/bench_string.c b/benchmarks/bench_string.c new file mode 100644 index 0000000..9cae993 --- /dev/null +++ b/benchmarks/bench_string.c @@ -0,0 +1,153 @@ +/* + * Benchmark harness for the five L1 string functions — strlen, strcmp, + * memcpy, memmove, memset (musts/BENCHMARKING.md). + * + * Deliberately a SYSTEM-headers-only translation unit: it includes no vlibc + * header, because vlibc's self-contained / (guard + * VLIBC_STDDEF_H) conflicts with the system / (guard + * _STDDEF_H) — both define size_t/NULL/offsetof/max_align_t and mixing them + * hard-errors. Which libc implements the timed calls is therefore decided + * purely at link time (../libvlibc.la vs the system libc). + */ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#define ITERATIONS 50000000ULL + +/* + * Volatile function pointers defeat both GCC builtin substitution and + * pure-attribute LICM hoisting of the timed loops (-fno-builtin alone does + * not prevent the latter). The signatures match the system-header + * declarations exactly: memcpy has restrict, memmove does not. + */ +static size_t (*volatile p_strlen)(const char *) = strlen; +static int (*volatile p_strcmp)(const char *, const char *) = strcmp; +static void *(*volatile p_memcpy)(void *restrict, const void *restrict, size_t) = memcpy; +static void *(*volatile p_memmove)(void *, const void *, size_t) = memmove; +static void *(*volatile p_memset)(void *, int, size_t) = memset; + +/* + * XOR accumulator. Every timed result is folded in here so the compiler + * cannot discard the indirect calls; volatile keeps the store itself alive. + */ +static volatile unsigned long long sink; + +/* Fixed small inputs: 8-byte and 64-byte buffers, sized per the plan. */ +static char str8[8] = "abcdefg"; /* 7 non-NUL bytes + NUL: full scan */ +static char str64[64] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789."; +static char cmp8a[8] = "abcdefg"; +static char cmp8b[8] = "abcdefg"; /* equal: strcmp runs the full length */ +static char cmp64a[64] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789."; +static char cmp64b[64] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789."; +static char src8[8]; +static char src64[64]; +static char dst8[8]; +static char dst64[64]; + +/* + * Time one expression executed ITERATIONS times through a volatile function + * pointer, then print `name@size ns/call`. The result of each call is XORed + * into a local accumulator and folded into `sink` after the loop, so the + * calls cannot be eliminated. On clock_gettime failure, returns 1 out of + * main. + */ +#define MEASURE(name, size, call) \ + do \ + { \ + unsigned long long r = 0; \ + double seconds; \ + if (clock_gettime(CLOCK_MONOTONIC, &start) != 0) \ + { \ + return 1; \ + } \ + for (unsigned long long i = 0; i < ITERATIONS; i++) \ + { \ + r ^= (unsigned long long)(call); \ + } \ + if (clock_gettime(CLOCK_MONOTONIC, &end) != 0) \ + { \ + return 1; \ + } \ + sink ^= r; \ + seconds = (double)(end.tv_sec - start.tv_sec) + \ + (double)(end.tv_nsec - start.tv_nsec) / 1000000000.0; \ + printf("%s@%d %.1f\n", name, size, \ + seconds * 1000000000.0 / (double)ITERATIONS); \ + } while (0) + +int +main(void) +{ + struct timespec start; + struct timespec end; + + /* + * Correctness self-asserts, routed through the SAME volatile function + * pointers as the timed loops. A bare literal call (strlen("hello")) + * would be constant-folded by GCC even at -O0, and a call through a + * non-volatile pointer would not exercise the pointer the timing uses. + */ + { + char scratch[16]; + + if (p_strlen("hello") != 5) + { + return 1; + } + if (p_strcmp("a", "b") >= 0) + { + return 1; + } + if (p_strcmp("abc", "abc") != 0) + { + return 1; + } + if (p_memset(scratch, 0, sizeof scratch) != scratch) + { + return 1; + } + if (p_memcpy(scratch, "hello", 6) != scratch) + { + return 1; + } + if (p_strcmp(scratch, "hello") != 0) + { + return 1; + } + if (p_memmove(scratch + 1, scratch, 6) != scratch + 1) + { + return 1; + } + if (p_strcmp(scratch + 1, "hello") != 0) + { + return 1; + } + } + + MEASURE("strlen", 8, p_strlen(str8)); + MEASURE("strlen", 64, p_strlen(str64)); + MEASURE("strcmp", 8, p_strcmp(cmp8a, cmp8b)); + MEASURE("strcmp", 64, p_strcmp(cmp64a, cmp64b)); + MEASURE("memcpy", 8, p_memcpy(dst8, src8, sizeof dst8)); + MEASURE("memcpy", 64, p_memcpy(dst64, src64, sizeof dst64)); + MEASURE("memmove", 8, p_memmove(dst8, src8, sizeof dst8)); + MEASURE("memmove", 64, p_memmove(dst64, src64, sizeof dst64)); + MEASURE("memset", 8, p_memset(dst8, 0x5a, sizeof dst8)); + MEASURE("memset", 64, p_memset(dst64, 0x5a, sizeof dst64)); + + /* Keep the accumulated results observable to the compiler. */ + if (sink == 0xdeadbeefULL) + { + printf("%llu\n", sink); + } + + return 0; +}