build: replace premake with a hand-written Makefile; optimize FFT
Build system - Add a single hand-written Makefile (GNU make + gcc, pure C). Builds raylib from vendored source and links rspektrum with no premake/lua. Targets: all/run/test/bench/clean; release default, DEBUG=1 for debug; ARCH overridable (defaults to -march=x86-64-v3). - Remove premake entirely: rspektrum.make, raylib.make, build/premake5* binaries, build/premake5.lua, build/ecc/*. The generated top-level Makefile was gitignored, so hand-edits to it were silently lost. - Vendor raylib src/ into the repo (was gitignored -> fresh clones could not build). Commit only src/ (~16MB); examples/projects stay local. Verified: a build from the git-tracked tree alone succeeds offline. - Release flags bumped to -O3 -ffast-math with a portable arch baseline (x86-64-v3 = AVX2+FMA on x64, SSE2 on x86). Confirmed FMA/AVX codegen in fft.o. FFT optimization (src/fft.c) - Precompute twiddle factors and the bit-reversal permutation once per size, cached as a small plan table (FFTW's idea, lightweight). Removes the per-butterfly cexpf() and per-element bit-twiddling that dominated. - 3.6x faster on the mlnl_samples.wav STFT workload (2048-pt, -O2 same flags both sides): 81us -> 22us per FFT. With the new -O3/-ffast-math/ AVX2 release flags stacked: ~15us (5.5x vs the old -O2 baseline). - Verified vs a double-precision reference DFT: 1e-6 relative error, round-trip 2.4e-7. Drop-in: same FFT() signature. Tests/bench (bench/) - fft_verify.c: FFT vs reference DFT + round-trip check (make test). - fft_bench.c: times the real STFT workload (make bench). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Compiled benchmark/test binaries
|
||||
fft_verify
|
||||
fft_bench_before
|
||||
fft_bench_after
|
||||
fft_bench_stacked
|
||||
fft_bench
|
||||
@@ -0,0 +1,32 @@
|
||||
# DSP tests + benchmarks for the standalone FFT (src/fft.c has no app/raylib deps).
|
||||
#
|
||||
# The repo's top-level Makefile is Premake-generated and gitignored, so the DSP
|
||||
# test target lives here where it's version-controlled.
|
||||
#
|
||||
# make -C bench test # correctness: FFT vs double-precision reference DFT
|
||||
# make -C bench bench # timing over the real mlnl_samples.wav STFT workload
|
||||
#
|
||||
# Same release flags the app uses, so this exercises the real codegen path
|
||||
# (-ffast-math reordering included). Override CC/CFLAGS to test other toolchains.
|
||||
CC ?= cc
|
||||
CFLAGS ?= -O3 -ffast-math -march=x86-64-v3 -Wall
|
||||
LDLIBS := -lm
|
||||
|
||||
SRC := ../src/fft.c
|
||||
|
||||
.PHONY: test bench clean
|
||||
|
||||
test: fft_verify
|
||||
./fft_verify
|
||||
|
||||
bench: fft_bench
|
||||
./fft_bench
|
||||
|
||||
fft_verify: fft_verify.c $(SRC)
|
||||
$(CC) $(CFLAGS) $^ $(LDLIBS) -o $@
|
||||
|
||||
fft_bench: fft_bench.c $(SRC)
|
||||
$(CC) $(CFLAGS) $^ $(LDLIBS) -o $@
|
||||
|
||||
clean:
|
||||
rm -f fft_verify fft_bench
|
||||
@@ -0,0 +1,96 @@
|
||||
// fft_bench.c - Isolated benchmark of FFT() over the real mlnl_samples.wav STFT
|
||||
// workload (2048-pt forward transforms, hop = fftSize/4, 75% overlap).
|
||||
//
|
||||
// Build: cc -O2 -march=native fft_bench.c ../src/fft.c -lm -o fft_bench
|
||||
// (link whichever fft.c you want to measure; -DFFT_BENCH_REPS=N to override reps)
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <complex.h>
|
||||
#include <math.h>
|
||||
#include <time.h>
|
||||
#include "../src/fft.h"
|
||||
|
||||
#define FFT_SIZE 2048
|
||||
#define HOP_RATIO 4
|
||||
#ifndef FFT_BENCH_REPS
|
||||
#define FFT_BENCH_REPS 50 // repeat the whole STFT pass this many times
|
||||
#endif
|
||||
|
||||
// Minimal WAV loader: 16-bit PCM, returns mono float frames (first channel).
|
||||
static float* load_wav(const char* path, int* outN) {
|
||||
FILE* f = fopen(path, "rb");
|
||||
if (!f) { perror("open wav"); exit(1); }
|
||||
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
|
||||
uint8_t* buf = malloc(sz);
|
||||
if (fread(buf, 1, sz, f) != (size_t)sz) { fprintf(stderr, "read fail\n"); exit(1); }
|
||||
fclose(f);
|
||||
|
||||
// Walk RIFF chunks to find fmt + data.
|
||||
int channels = 1, bits = 16; uint32_t dataOff = 0, dataLen = 0;
|
||||
uint32_t p = 12; // skip RIFF....WAVE
|
||||
while (p + 8 <= (uint32_t)sz) {
|
||||
uint32_t cid; memcpy(&cid, buf + p, 4);
|
||||
uint32_t clen; memcpy(&clen, buf + p + 4, 4);
|
||||
if (memcmp(buf + p, "fmt ", 4) == 0) {
|
||||
memcpy(&channels, buf + p + 8 + 2, 2);
|
||||
memcpy(&bits, buf + p + 8 + 14, 2);
|
||||
} else if (memcmp(buf + p, "data", 4) == 0) {
|
||||
dataOff = p + 8; dataLen = clen; break;
|
||||
}
|
||||
p += 8 + clen + (clen & 1);
|
||||
}
|
||||
if (!dataOff || bits != 16) { fprintf(stderr, "need 16-bit PCM wav\n"); exit(1); }
|
||||
|
||||
int bytesPerSample = 2 * channels;
|
||||
int n = dataLen / bytesPerSample;
|
||||
float* out = malloc(n * sizeof(float));
|
||||
const int16_t* pcm = (const int16_t*)(buf + dataOff);
|
||||
for (int i = 0; i < n; i++) out[i] = pcm[i * channels] / 32768.0f;
|
||||
free(buf);
|
||||
*outN = n;
|
||||
return out;
|
||||
}
|
||||
|
||||
static double now_sec(void) {
|
||||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec + ts.tv_nsec * 1e-9;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const char* path = argc > 1 ? argv[1] : "../mlnl_samples.wav";
|
||||
int n; float* samples = load_wav(path, &n);
|
||||
|
||||
int hop = FFT_SIZE / HOP_RATIO;
|
||||
int frames = (n - FFT_SIZE) / hop + 1;
|
||||
if (frames < 1) { fprintf(stderr, "too short\n"); return 1; }
|
||||
|
||||
float complex* in = malloc(FFT_SIZE * sizeof(float complex));
|
||||
float complex* out = malloc(FFT_SIZE * sizeof(float complex));
|
||||
|
||||
// Hann-windowed frames, exactly like stft.c feeds FFT().
|
||||
double checksum = 0.0;
|
||||
double t0 = now_sec();
|
||||
for (int rep = 0; rep < FFT_BENCH_REPS; rep++) {
|
||||
for (int fr = 0; fr < frames; fr++) {
|
||||
int off = fr * hop;
|
||||
for (int i = 0; i < FFT_SIZE; i++) {
|
||||
float w = 0.5f - 0.5f * cosf(2.0f * (float)M_PI * i / (FFT_SIZE - 1));
|
||||
in[i] = samples[off + i] * w + 0.0f * I;
|
||||
}
|
||||
FFT(in, out, FFT_SIZE, false);
|
||||
checksum += crealf(out[1]) + cimagf(out[1]);
|
||||
}
|
||||
}
|
||||
double t1 = now_sec();
|
||||
|
||||
long long totalFFTs = (long long)frames * FFT_BENCH_REPS;
|
||||
double secs = t1 - t0;
|
||||
printf("samples=%d frames=%d reps=%d total_FFTs=%lld\n",
|
||||
n, frames, FFT_BENCH_REPS, totalFFTs);
|
||||
printf("elapsed=%.4f s per-FFT=%.3f us FFTs/s=%.0f\n",
|
||||
secs, secs / totalFFTs * 1e6, totalFFTs / secs);
|
||||
printf("checksum=%.6f (anti-DCE)\n", checksum);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// fft_verify.c - correctness check for FFT(): compare vs naive DFT, and check
|
||||
// the forward->inverse round-trip recovers the input.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <complex.h>
|
||||
#include "../src/fft.h"
|
||||
|
||||
// Reference DFT accumulated in double precision so the *reference* isn't the
|
||||
// source of error when comparing a float FFT at large n.
|
||||
static void naive_dft(const float complex* x, float complex* X, int n) {
|
||||
for (int k = 0; k < n; k++) {
|
||||
double complex s = 0;
|
||||
for (int t = 0; t < n; t++)
|
||||
s += x[t] * cexp(-2.0 * M_PI * I * k * t / n);
|
||||
X[k] = (float complex)s;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
int sizes[] = {2, 4, 8, 16, 64, 256, 1024, 2048};
|
||||
double worst_dft = 0, worst_rt = 0;
|
||||
for (unsigned si = 0; si < sizeof(sizes)/sizeof(sizes[0]); si++) {
|
||||
int n = sizes[si];
|
||||
float complex* x = malloc(n*sizeof(float complex));
|
||||
float complex* X = malloc(n*sizeof(float complex));
|
||||
float complex* Xn = malloc(n*sizeof(float complex));
|
||||
float complex* xrt = malloc(n*sizeof(float complex));
|
||||
srand(1234 + n);
|
||||
for (int i = 0; i < n; i++)
|
||||
x[i] = (rand()/(float)RAND_MAX - 0.5f) + (rand()/(float)RAND_MAX - 0.5f)*I;
|
||||
|
||||
FFT(x, X, n, false);
|
||||
naive_dft(x, Xn, n);
|
||||
FFT(X, xrt, n, true);
|
||||
|
||||
double norm = 0;
|
||||
for (int i = 0; i < n; i++) norm += cabsf(Xn[i]);
|
||||
norm /= n; // mean magnitude, for a relative tolerance
|
||||
for (int i = 0; i < n; i++) {
|
||||
double e1 = cabsf(X[i] - Xn[i]) / norm; // relative to spectrum scale
|
||||
double e2 = cabsf(xrt[i] - x[i]);
|
||||
if (e1 > worst_dft) worst_dft = e1;
|
||||
if (e2 > worst_rt) worst_rt = e2;
|
||||
}
|
||||
free(x); free(X); free(Xn); free(xrt);
|
||||
}
|
||||
printf("max relative |FFT - naive DFT| = %.3e\n", worst_dft);
|
||||
printf("max round-trip error = %.3e\n", worst_rt);
|
||||
printf("%s\n", (worst_dft < 1e-4 && worst_rt < 1e-4) ? "PASS" : "FAIL");
|
||||
return (worst_dft < 1e-4 && worst_rt < 1e-4) ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user