Rewrite i386 backend as hand-written assembly

Replaces the C fallback (borrowed from PowerPC) with dedicated i386
assembly for all nine kernels, matching the amd64/arm64 convention.
Fixes two real bugs found and verified on physical Pentium 4 hardware:
scalar FP silently running on x87 instead of SSE2, and fm_int_math
calling __udivdi3 on every iteration of the highest-weighted test
because the C compiler didn't know its 0xdeadbeef divisor fits in 32
bits. Real-hardware SINGLECORE/MULTICORE scores go from 425/448 to
539/581.
This commit is contained in:
2026-07-18 04:35:03 -05:00
parent 136b417cf8
commit 357e6f0d6d
6 changed files with 1370 additions and 38 deletions
+1
View File
@@ -170,4 +170,5 @@ jobs:
release/SHA256SUMS \ release/SHA256SUMS \
--repo "$GITHUB_REPOSITORY" \ --repo "$GITHUB_REPOSITORY" \
--title "Release $RELEASE_TAG" \ --title "Release $RELEASE_TAG" \
--notes "⚠️ **i386 (Pentium 4) support is new in this release and still being validated on real hardware.** i386 benchmark scores should not be considered accurate yet." \
--generate-notes --generate-notes
+16 -6
View File
@@ -3,6 +3,7 @@
# The assembly kernels are architecture-specific: # The assembly kernels are architecture-specific:
# src/fossmark.S AArch64 (ARM64) # src/fossmark.S AArch64 (ARM64)
# src/fossmark_x86_64.S x86-64 (AMD64) # src/fossmark_x86_64.S x86-64 (AMD64)
# src/fossmark_i386.S x86 32-bit (i386, Pentium 4 baseline)
# src/fossmark_ppc32.c PowerPC 32-bit, including big-endian systems # src/fossmark_ppc32.c PowerPC 32-bit, including big-endian systems
# and the portable PPC64 kernel implementations # and the portable PPC64 kernel implementations
# The C driver (src/main.c) is portable across architectures and OSes. A # The C driver (src/main.c) is portable across architectures and OSes. A
@@ -48,7 +49,7 @@ DIST := dist
DRIVER := src/main.c DRIVER := src/main.c
ASM_ARM64 := src/fossmark.S ASM_ARM64 := src/fossmark.S
ASM_AMD64 := src/fossmark_x86_64.S ASM_AMD64 := src/fossmark_x86_64.S
SRC_I386 := src/fossmark_ppc32.c ASM_I386 := src/fossmark_i386.S
SRC_PPC32 := src/fossmark_ppc32.c SRC_PPC32 := src/fossmark_ppc32.c
ASM_PPC32 := src/fossmark_ppc32_ext.S ASM_PPC32 := src/fossmark_ppc32_ext.S
SRC_PPC64 := src/fossmark_ppc32.c SRC_PPC64 := src/fossmark_ppc32.c
@@ -63,7 +64,7 @@ else ifneq (,$(filter x86_64 amd64,$(HOST_ARCH)))
HOST_KERNEL := $(ASM_AMD64) HOST_KERNEL := $(ASM_AMD64)
else ifneq (,$(filter i386 i486 i586 i686 x86,$(HOST_ARCH))) else ifneq (,$(filter i386 i486 i586 i686 x86,$(HOST_ARCH)))
HOST_ARCHNAME := i386 HOST_ARCHNAME := i386
HOST_KERNEL := $(SRC_I386) HOST_KERNEL := $(ASM_I386)
else ifneq (,$(filter ppc powerpc ppc32 powerpc32,$(HOST_ARCH))) else ifneq (,$(filter ppc powerpc ppc32 powerpc32,$(HOST_ARCH)))
HOST_ARCHNAME := ppc32be HOST_ARCHNAME := ppc32be
HOST_KERNEL := $(SRC_PPC32) $(ASM_PPC32) HOST_KERNEL := $(SRC_PPC32) $(ASM_PPC32)
@@ -75,7 +76,16 @@ else
$(error unsupported host architecture '$(HOST_ARCH)') $(error unsupported host architecture '$(HOST_ARCH)')
endif endif
ifeq ($(HOST_ARCHNAME),i386) ifeq ($(HOST_ARCHNAME),i386)
CFLAGS += -march=pentium4 -msse2 # The kernels are hand-written assembly (fossmark_i386.S) using SSE2
# directly, so -msse2/-mfpmath=sse have nothing left to gate - only
# main.c (the portable driver) is still compiled from C here.
#
# -fno-pie: i386 PIC costs a whole general-purpose register (already the
# scarcest resource in 32-bit mode) for the life of any function that
# touches global data or calls out - a tax amd64/arm64 don't pay the same
# way. Paired with -no-pie at link time below.
CFLAGS += -march=pentium4 -fno-pie
LDFLAGS += -no-pie
endif endif
ifeq ($(HOST_ARCHNAME),ppc64be) ifeq ($(HOST_ARCHNAME),ppc64be)
CFLAGS += -mcpu=970 -maltivec CFLAGS += -mcpu=970 -maltivec
@@ -154,8 +164,8 @@ $(DIST)/fossmark-linux-amd64: $(DRIVER) $(ASM_AMD64) | $(DIST)
$(CC_AMD64) $(CFLAGS) $(TLS_CFLAGS) $(PTHREAD) $(LDFLAGS) -o $@ $(DRIVER) $(ASM_AMD64) $(LDLIBS) $(CC_AMD64) $(CFLAGS) $(TLS_CFLAGS) $(PTHREAD) $(LDFLAGS) -o $@ $(DRIVER) $(ASM_AMD64) $(LDLIBS)
@echo "built $@" @echo "built $@"
$(DIST)/fossmark-linux-i386: $(DRIVER) $(SRC_I386) | $(DIST) $(DIST)/fossmark-linux-i386: $(DRIVER) $(ASM_I386) | $(DIST)
$(CC_I386) -m32 -march=pentium4 -msse2 $(CFLAGS) $(TLS_CFLAGS) $(PTHREAD) $(LDFLAGS) -o $@ $(DRIVER) $(SRC_I386) $(LDLIBS) $(CC_I386) -m32 -march=pentium4 -fno-pie -no-pie $(CFLAGS) $(TLS_CFLAGS) $(PTHREAD) $(LDFLAGS) -o $@ $(DRIVER) $(ASM_I386) $(LDLIBS)
@echo "built $@" @echo "built $@"
$(DIST)/fossmark-linux-ppc32be: $(DRIVER) $(SRC_PPC32) $(ASM_PPC32) | $(DIST) $(DIST)/fossmark-linux-ppc32be: $(DRIVER) $(SRC_PPC32) $(ASM_PPC32) | $(DIST)
@@ -213,7 +223,7 @@ bench: $(NATIVE_BIN)
# Build and run the kernel correctness tests for the host arch. # Build and run the kernel correctness tests for the host arch.
test: | $(DIST) test: | $(DIST)
$(CC) $(CFLAGS) $(PTHREAD) -o $(DIST)/test_kernels src/test_kernels.c $(HOST_KERNEL) -lm $(CC) $(CFLAGS) $(PTHREAD) $(LDFLAGS) -o $(DIST)/test_kernels src/test_kernels.c $(HOST_KERNEL) -lm
./$(DIST)/test_kernels ./$(DIST)/test_kernels
clean: clean:
+6
View File
@@ -164,6 +164,11 @@ The kernel backends use only baseline instructions for their architecture:
* `src/fossmark.S` uses ARMv8-A and NEON under AAPCS64. * `src/fossmark.S` uses ARMv8-A and NEON under AAPCS64.
* `src/fossmark_x86_64.S` uses baseline x86-64 and SSE2 under the System V ABI. * `src/fossmark_x86_64.S` uses baseline x86-64 and SSE2 under the System V ABI.
* `src/fossmark_i386.S` uses baseline 32-bit x86 (Pentium 4) and SSE2 under the
i386 System V (cdecl) ABI. With only six general-purpose registers, no
64-bit integer registers, and half of amd64's SSE2 register file (xmm0-7),
several kernels keep working state on the stack instead of in registers -
a real cost of the architecture, not an oversight.
* `src/fossmark_ppc32.c` is endian-safe and keeps a baseline 32-bit PowerPC * `src/fossmark_ppc32.c` is endian-safe and keeps a baseline 32-bit PowerPC
fallback. At runtime, the extended-instruction test uses Paired Singles when fallback. At runtime, the extended-instruction test uses Paired Singles when
the device-tree `compatible` property begins with `nintendo,`; otherwise it the device-tree `compatible` property begins with `nintendo,`; otherwise it
@@ -205,6 +210,7 @@ with a nonzero status if any check fails.
src/main.c portable benchmark driver and scoring src/main.c portable benchmark driver and scoring
src/fossmark.S ARM64 kernels src/fossmark.S ARM64 kernels
src/fossmark_x86_64.S x86-64 kernels src/fossmark_x86_64.S x86-64 kernels
src/fossmark_i386.S i386 (Pentium 4) kernels
src/fossmark_ppc32.c PPC32/PPC64 big-endian kernels src/fossmark_ppc32.c PPC32/PPC64 big-endian kernels
src/fossmark_ppc32_ext.S optional PPC32 PS, VSX, and AltiVec kernels src/fossmark_ppc32_ext.S optional PPC32 PS, VSX, and AltiVec kernels
src/test_kernels.c correctness suite src/test_kernels.c correctness suite
+1344
View File
File diff suppressed because it is too large Load Diff
+2 -31
View File
@@ -11,10 +11,6 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#if defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64)
#include <emmintrin.h>
#endif
#if defined(__linux__) #if defined(__linux__)
#include <sys/auxv.h> #include <sys/auxv.h>
#endif #endif
@@ -68,7 +64,7 @@ uint64_t fm_primes(uint64_t limit, uint8_t *sieve)
return count; return count;
} }
#if defined(__powerpc__) && !defined(__powerpc64__) #if !defined(__powerpc64__)
static uint64_t fm_simd_scalar(uint64_t iters, void *memory) static uint64_t fm_simd_scalar(uint64_t iters, void *memory)
{ {
uint32_t *v = (uint32_t *)memory; uint32_t *v = (uint32_t *)memory;
@@ -113,7 +109,7 @@ uint64_t fm_simd(uint64_t iters, void *memory)
sum ^= v[j]; sum ^= v[j];
return sum; return sum;
} }
#elif defined(__powerpc__) #else
/* These are kept in fossmark_ppc32_ext.S so this translation unit, and thus /* These are kept in fossmark_ppc32_ext.S so this translation unit, and thus
* the executable's default code path, only requires baseline PPC32. */ * the executable's default code path, only requires baseline PPC32. */
extern void fm_simd_ps_kernel(uint64_t iters, void *memory); extern void fm_simd_ps_kernel(uint64_t iters, void *memory);
@@ -196,31 +192,6 @@ uint64_t fm_simd(uint64_t iters, void *memory)
sum ^= v[j]; sum ^= v[j];
return sum; return sum;
} }
#else
uint64_t fm_simd(uint64_t iters, void *memory)
{
__m128i a, b;
uint32_t *v = (uint32_t *)memory;
uint32_t sum = 0;
uint64_t i;
unsigned j;
if (!iters)
return 0;
a = _mm_loadu_si128((const __m128i *)v);
b = _mm_loadu_si128((const __m128i *)(v + 4));
for (i = 0; i < iters; i++) {
a = _mm_add_epi32(a, b);
b = _mm_xor_si128(b, a);
a = _mm_add_epi32(a, b);
b = _mm_xor_si128(b, a);
}
_mm_storeu_si128((__m128i *)v, a);
_mm_storeu_si128((__m128i *)(v + 4), b);
for (j = 0; j < 8; j++)
sum ^= v[j];
return sum;
}
#endif #endif
static uint32_t load32_native(const uint8_t *p) static uint32_t load32_native(const uint8_t *p)
+1 -1
View File
@@ -42,7 +42,7 @@
#ifndef FM_API_BASE_URL #ifndef FM_API_BASE_URL
# define FM_API_BASE_URL "https://fossbench.net" # define FM_API_BASE_URL "https://fossbench.net"
#endif #endif
#define FM_VERSION "0.1.3-hotfix4" #define FM_VERSION "0.1.4"
/* ---------- platform identification (for the banner only) ---------- */ /* ---------- platform identification (for the banner only) ---------- */