From c35ec1451ff28307e2c311c8c8281a82774b1e2f Mon Sep 17 00:00:00 2001 From: Owen Rummage Date: Mon, 20 Jul 2026 14:51:56 -0500 Subject: [PATCH] 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 --- src/app/benchmark.c | 45 ++++++++++++++++++---- src/app/hw_detect.c | 92 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 8 deletions(-) diff --git a/src/app/benchmark.c b/src/app/benchmark.c index 3a00361..70c6154 100644 --- a/src/app/benchmark.c +++ b/src/app/benchmark.c @@ -40,6 +40,27 @@ #endif #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) # define D_INT "64-bit ALU: madd, umulh, udiv, bitops" # 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); } +FB_SCALAR_KERNEL static uint64_t run_int32(uint64_t n, struct workspace *ws) { uint32_t a = 0x9e3779b9u, b = 0xbf58476du; uint32_t c = 0x94d049bbu, d = 0x2545f491u; uint64_t i, iters = n * 100000; (void)ws; + FB_SCALAR_LOOP for (i = 0; i < iters; i++) { a = a * 0xdeadbeefu + b; 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); } +FB_SCALAR_KERNEL static uint64_t run_stream(uint64_t n, struct workspace *ws) { 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 b = ws->stream_b; float *restrict c = ws->stream_c; + FB_SCALAR_LOOP for (i = 0; i < STREAM_N; 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; 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; - int i; + int i, samples; /* Increase the work until it runs long enough. */ for (;;) { @@ -670,8 +695,11 @@ static struct result run_test(const struct test *t, int threads) } } - /* Keep the fastest run. */ - best = elapsed; + /* Average every measured run. Keeping only the fastest rewarded a single + * 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++) { double t0 = now_seconds(); 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); exit(2); } - if (e < best) - best = e; + total += e; + samples++; } + average = total / samples; - r.seconds = best; + r.seconds = average; r.iters = n; r.checksum = checksum; r.threads = threads; /* 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; } diff --git a/src/app/hw_detect.c b/src/app/hw_detect.c index b52c6e3..571963c 100644 --- a/src/app/hw_detect.c +++ b/src/app/hw_detect.c @@ -17,6 +17,9 @@ #endif #if defined(_WIN32) # define WIN32_LEAN_AND_MEAN +# ifndef _WIN32_WINNT +# define _WIN32_WINNT 0x0601 /* GetLogicalProcessorInformationEx (Win7+). */ +# endif # include # if defined(__i386__) || defined(__x86_64__) # include @@ -251,6 +254,83 @@ static int read_first_property(const char *path, char *dst, size_t cap) } #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) { long detected_threads; @@ -363,6 +443,13 @@ void hw_detect_system(struct system_info *info) 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]) snprintf(info->cpu, sizeof info->cpu, "%s", apple_soc); #if defined(__aarch64__) || defined(__arm__) @@ -479,6 +566,11 @@ void hw_detect_system(struct system_info *info) RegCloseKey(key); } } + { + long cores = win_physical_cores(); + if (cores > 0) + info->cpu_cores = cores; + } { MEMORYSTATUSEX ms; ms.dwLength = sizeof(ms);