Fix a few things that were throwing off benchmark scores

Two of the workloads (Native Integer Math and Memory Bandwidth) were
plain C rather than assembly, so the compiler's auto-vectorizer got to
decide how fast they ran. Newer Clang basically tripled the integer
number and blew up the STREAM triad by ~3x, which meant the same chip
landed all over the place depending on how it was built. Pinned both to
scalar codegen so GCC and Clang line up again.

Also stopped run_test from keeping only the fastest repeat -- it was
rewarding one lucky pass and hiding the normal run-to-run wobble. It
averages the repeats now.

And core detection was just wrong on a couple platforms. Windows was
handing back logical processors as physical cores (so an 8c/16t part
showed up as 16 cores), and Linux boxes where /proc/cpuinfo doesn't
carry topology (PowerPC, ARM) fell back to the thread count too -- a
176-thread POWER8 claimed 176 cores. Windows now counts real cores via
GetLogicalProcessorInformationEx, and Linux falls back to sysfs
thread-sibling groups, which also handles Apple Silicon's per-cluster
core_id numbering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 14:54:20 -05:00
co-authored by Claude Opus 4.8
parent 6e21ee3574
commit c35ec1451f
2 changed files with 129 additions and 8 deletions
+37 -8
View File
@@ -40,6 +40,27 @@
#endif #endif
#define FB_VERSION "0.3.0" #define FB_VERSION "0.3.0"
/*
* Native Integer Math and Memory Bandwidth are the only two *measured*
* workloads written in C instead of a per-architecture assembly kernel. Because
* they were compiled with whatever optimizer the build used, auto-vectorization
* made their results depend on the compiler rather than the CPU: newer Clang
* roughly tripled the integer result and inflated the STREAM triad by ~3x,
* so the same machine scored very differently across builds. Pin both to
* deterministic scalar code so every compiler measures the same work. These two
* carry weight in web/src/scoring.js, so their stability matters to the score.
*/
#if defined(__GNUC__) && !defined(__clang__)
# define FB_SCALAR_KERNEL __attribute__((optimize("O2", "no-tree-vectorize", "no-tree-slp-vectorize")))
#else
# define FB_SCALAR_KERNEL
#endif
#if defined(__clang__)
# define FB_SCALAR_LOOP _Pragma("clang loop vectorize(disable) interleave(disable)")
#else
# define FB_SCALAR_LOOP
#endif
#if defined(__aarch64__) || defined(__arm64__) || defined(_M_ARM64) #if defined(__aarch64__) || defined(__arm64__) || defined(_M_ARM64)
# define D_INT "64-bit ALU: madd, umulh, udiv, bitops" # define D_INT "64-bit ALU: madd, umulh, udiv, bitops"
# define D_FP "double: fmadd, fdiv, fsqrt" # define D_FP "double: fmadd, fdiv, fsqrt"
@@ -448,12 +469,14 @@ static uint64_t run_int(uint64_t n, struct workspace *ws)
return fb_int_math(n * 100000); return fb_int_math(n * 100000);
} }
FB_SCALAR_KERNEL
static uint64_t run_int32(uint64_t n, struct workspace *ws) static uint64_t run_int32(uint64_t n, struct workspace *ws)
{ {
uint32_t a = 0x9e3779b9u, b = 0xbf58476du; uint32_t a = 0x9e3779b9u, b = 0xbf58476du;
uint32_t c = 0x94d049bbu, d = 0x2545f491u; uint32_t c = 0x94d049bbu, d = 0x2545f491u;
uint64_t i, iters = n * 100000; uint64_t i, iters = n * 100000;
(void)ws; (void)ws;
FB_SCALAR_LOOP
for (i = 0; i < iters; i++) { for (i = 0; i < iters; i++) {
a = a * 0xdeadbeefu + b; a = a * 0xdeadbeefu + b;
b = b * 0xdeadbeefu + c; b = b * 0xdeadbeefu + c;
@@ -516,6 +539,7 @@ static uint64_t run_chase(uint64_t n, struct workspace *ws)
return fb_chase(ws->chase, n * 1000000); return fb_chase(ws->chase, n * 1000000);
} }
FB_SCALAR_KERNEL
static uint64_t run_stream(uint64_t n, struct workspace *ws) static uint64_t run_stream(uint64_t n, struct workspace *ws)
{ {
uint64_t pass, checksum = 0; uint64_t pass, checksum = 0;
@@ -525,6 +549,7 @@ static uint64_t run_stream(uint64_t n, struct workspace *ws)
float *restrict a = ws->stream_a; float *restrict a = ws->stream_a;
float *restrict b = ws->stream_b; float *restrict b = ws->stream_b;
float *restrict c = ws->stream_c; float *restrict c = ws->stream_c;
FB_SCALAR_LOOP
for (i = 0; i < STREAM_N; i++) for (i = 0; i < STREAM_N; i++)
c[i] = a[i] + scale * b[i]; c[i] = a[i] + scale * b[i];
} }
@@ -646,9 +671,9 @@ static struct result run_test(const struct test *t, int threads)
{ {
struct result r; struct result r;
uint64_t n = t->start_n; uint64_t n = t->start_n;
double elapsed = 0.0, best = 0.0; double elapsed = 0.0, total = 0.0, average;
uint64_t checksum = 0; uint64_t checksum = 0;
int i; int i, samples;
/* Increase the work until it runs long enough. */ /* Increase the work until it runs long enough. */
for (;;) { for (;;) {
@@ -670,8 +695,11 @@ static struct result run_test(const struct test *t, int threads)
} }
} }
/* Keep the fastest run. */ /* Average every measured run. Keeping only the fastest rewarded a single
best = elapsed; * lucky sample and hid the run-to-run variation that real machines show;
* the mean is a more representative and reproducible throughput. */
total = elapsed;
samples = 1;
for (i = 1; i < REPEATS; i++) { for (i = 1; i < REPEATS; i++) {
double t0 = now_seconds(); double t0 = now_seconds();
uint64_t c = dispatch(t->run, n, threads); uint64_t c = dispatch(t->run, n, threads);
@@ -685,16 +713,17 @@ static struct result run_test(const struct test *t, int threads)
(unsigned long long)checksum); (unsigned long long)checksum);
exit(2); exit(2);
} }
if (e < best) total += e;
best = e; samples++;
} }
average = total / samples;
r.seconds = best; r.seconds = average;
r.iters = n; r.iters = n;
r.checksum = checksum; r.checksum = checksum;
r.threads = threads; r.threads = threads;
/* Calculate the total speed. */ /* Calculate the total speed. */
r.rate = ((double)threads * (double)n * t->work_per_n) / best / 1e6; r.rate = ((double)threads * (double)n * t->work_per_n) / average / 1e6;
return r; return r;
} }
+92
View File
@@ -17,6 +17,9 @@
#endif #endif
#if defined(_WIN32) #if defined(_WIN32)
# define WIN32_LEAN_AND_MEAN # define WIN32_LEAN_AND_MEAN
# ifndef _WIN32_WINNT
# define _WIN32_WINNT 0x0601 /* GetLogicalProcessorInformationEx (Win7+). */
# endif
# include <windows.h> # include <windows.h>
# if defined(__i386__) || defined(__x86_64__) # if defined(__i386__) || defined(__x86_64__)
# include <cpuid.h> # include <cpuid.h>
@@ -251,6 +254,83 @@ static int read_first_property(const char *path, char *dst, size_t cap)
} }
#endif #endif
#if defined(_WIN32)
/* Count physical cores. Windows only exposes logical processors through
* GetSystemInfo, so with SMT/HyperThreading every count was wrong (an 8-core /
* 16-thread part reported 16 cores). Each RelationProcessorCore record returned
* by GetLogicalProcessorInformationEx describes exactly one physical core. */
static long win_physical_cores(void)
{
DWORD length = 0;
BYTE *buffer, *p;
long cores = 0;
if (GetLogicalProcessorInformationEx(RelationProcessorCore, NULL, &length))
return 0;
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || length == 0)
return 0;
buffer = malloc(length);
if (buffer == NULL)
return 0;
if (GetLogicalProcessorInformationEx(RelationProcessorCore,
(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)buffer, &length)) {
for (p = buffer; p < buffer + length; ) {
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX record =
(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)p;
if (record->Size == 0)
break; /* Guard against a malformed run. */
if (record->Relationship == RelationProcessorCore)
cores++;
p += record->Size;
}
}
free(buffer);
return cores;
}
#endif
#if defined(__linux__)
/* Count physical cores from sysfs topology. PowerPC (and some ARM) kernels omit
* the "physical id" / "core id" fields from /proc/cpuinfo, so those hosts fell
* back to the thread count (a POWER8 with SMT8 reported 176 cores instead of
* 22). Every thread of a physical core shares one thread-sibling group, and the
* lowest thread id in that group uniquely identifies the core -- counting the
* distinct groups is correct with SMT, without it, and across clusters whose
* core_id numbering restarts (e.g. Apple Silicon under Asahi). */
static long linux_topology_cores(long max_threads)
{
long seen[4096];
int nseen = 0;
long cpu, cores = 0;
for (cpu = 0; cpu < max_threads && cpu < 4096; cpu++) {
char path[192], value[256];
long first;
int i, duplicate = 0;
snprintf(path, sizeof path,
"/sys/devices/system/cpu/cpu%ld/topology/thread_siblings_list", cpu);
if (!read_first_property(path, value, sizeof value)) {
snprintf(path, sizeof path,
"/sys/devices/system/cpu/cpu%ld/topology/core_cpus_list", cpu);
if (!read_first_property(path, value, sizeof value))
continue;
}
first = strtol(value, NULL, 0); /* Lowest thread id of this core. */
for (i = 0; i < nseen; i++)
if (seen[i] == first) {
duplicate = 1;
break;
}
if (!duplicate && nseen < 4096) {
seen[nseen++] = first;
cores++;
}
}
return cores;
}
#endif
void hw_detect_system(struct system_info *info) void hw_detect_system(struct system_info *info)
{ {
long detected_threads; long detected_threads;
@@ -363,6 +443,13 @@ void hw_detect_system(struct system_info *info)
if (npairs > 0) info->cpu_cores = npairs; if (npairs > 0) info->cpu_cores = npairs;
} }
} }
/* When /proc/cpuinfo did not distinguish cores from threads (PowerPC, ARM),
* recover the physical core count from sysfs topology. */
if (info->cpu_cores == info->cpu_threads) {
long cores = linux_topology_cores(info->cpu_threads);
if (cores > 0)
info->cpu_cores = cores;
}
if (apple_soc[0]) if (apple_soc[0])
snprintf(info->cpu, sizeof info->cpu, "%s", apple_soc); snprintf(info->cpu, sizeof info->cpu, "%s", apple_soc);
#if defined(__aarch64__) || defined(__arm__) #if defined(__aarch64__) || defined(__arm__)
@@ -479,6 +566,11 @@ void hw_detect_system(struct system_info *info)
RegCloseKey(key); RegCloseKey(key);
} }
} }
{
long cores = win_physical_cores();
if (cores > 0)
info->cpu_cores = cores;
}
{ {
MEMORYSTATUSEX ms; MEMORYSTATUSEX ms;
ms.dwLength = sizeof(ms); ms.dwLength = sizeof(ms);