bench(string): add bench_string.c vs glibc/musl
System-headers-only harness timing the five L1 string functions (strlen, strcmp, memcpy, memmove, memset) at 8- and 64-byte inputs. Calls go through volatile function pointers to defeat builtin substitution and LICM hoisting; correctness self-asserts route through the same pointers. Prints FUNC@SIZE ns/call lines. bench_string uses empty per-target CPPFLAGS (no -I include) so it links against whichever libc --with-libc= selects; the bench target runs it always and bench_vlibc only under BENCH_LINK_VLIBC. Both programs relink when config.status changes, so a reconfigure cannot silently reuse a binary from the previous libc. Measured vs glibc 2.44 (median of 3 runs): mem* 1.0-3.3x, strlen/strcmp 3.2-3.9x — word-at-a-time vs AVX2, all sane. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
This commit is contained in:
@@ -105,4 +105,5 @@ Makefile.in
|
|||||||
|
|
||||||
# Built benchmark harnesses (benchmarks/bench_* are generated from bench_*.c).
|
# Built benchmark harnesses (benchmarks/bench_* are generated from bench_*.c).
|
||||||
/benchmarks/bench_vlibc
|
/benchmarks/bench_vlibc
|
||||||
|
/benchmarks/bench_string
|
||||||
|
|
||||||
|
|||||||
+28
-2
@@ -5,23 +5,49 @@ AM_CFLAGS = @VLIBC_CFLAGS@
|
|||||||
|
|
||||||
# Built only on demand (via `make bench`), so `make all` does not need the
|
# Built only on demand (via `make bench`), so `make all` does not need the
|
||||||
# library to be built first.
|
# library to be built first.
|
||||||
EXTRA_PROGRAMS = bench_vlibc
|
EXTRA_PROGRAMS = bench_vlibc bench_string
|
||||||
bench_vlibc_SOURCES = bench_vlibc.c
|
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 <stddef.h>/<string.h> into the same TU as
|
||||||
|
# the system <stdio.h>/<time.h> 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
|
# By default benchmarks link against vlibc itself. --with-libc=glibc links the
|
||||||
# same harness against the system glibc; --with-libc=musl builds it with
|
# same harness against the system glibc; --with-libc=musl builds it with
|
||||||
# musl-gcc for the musl reference.
|
# musl-gcc for the musl reference.
|
||||||
if BENCH_LINK_VLIBC
|
if BENCH_LINK_VLIBC
|
||||||
bench_vlibc_LDADD = ../libvlibc.la
|
bench_vlibc_LDADD = ../libvlibc.la
|
||||||
|
bench_string_LDADD = ../libvlibc.la
|
||||||
else
|
else
|
||||||
bench_vlibc_LDADD =
|
bench_vlibc_LDADD =
|
||||||
|
bench_string_LDADD =
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if BENCH_LINK_MUSL
|
if BENCH_LINK_MUSL
|
||||||
CC = $(MUSL_CC)
|
CC = $(MUSL_CC)
|
||||||
endif
|
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_vlibc
|
||||||
|
./bench_string
|
||||||
|
else
|
||||||
|
bench: bench_string
|
||||||
|
./bench_string
|
||||||
|
endif
|
||||||
|
|
||||||
.PHONY: bench
|
.PHONY: bench
|
||||||
|
|||||||
@@ -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 <stddef.h>/<string.h> (guard
|
||||||
|
* VLIBC_STDDEF_H) conflicts with the system <stdio.h>/<time.h> (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 <config.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user