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);