From be0753f9d87a233df634bcbaafcfde7c0584b72d Mon Sep 17 00:00:00 2001 From: lbaumberger Date: Thu, 28 May 2026 22:49:08 +0200 Subject: [PATCH] fix for the two main files --- Makefile | 12 +- main-float_symmetry.cpp | 294 ++++++++++++++++++++++++++++++++++++++++ main-symmetry.cpp | 287 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 592 insertions(+), 1 deletion(-) create mode 100644 main-float_symmetry.cpp create mode 100644 main-symmetry.cpp diff --git a/Makefile b/Makefile index c939545..f569a5f 100644 --- a/Makefile +++ b/Makefile @@ -63,11 +63,21 @@ endif VARIANT_SRC := ./variants/variant_$(VARIANT).cpp +# symmetry variants do half the pairwise work, so they use their own main with the +# halved flop-count formula. float_ symmetry variants use the float symmetry main. ifneq ($(filter float_%,$(VARIANT)),) +ifneq ($(findstring symmetry,$(VARIANT)),) +MAIN_SRC := main-float_symmetry.cpp +else MAIN_SRC := main-float.cpp +endif +else +ifneq ($(findstring symmetry,$(VARIANT)),) +MAIN_SRC := main-symmetry.cpp else MAIN_SRC := main.cpp endif +endif SRC := $(MAIN_SRC) $(VARIANT_SRC) HEADERS := ./headers/board.hpp ./headers/pedestrian.hpp ./headers/vector2d.hpp ./headers/constants.hpp ./headers/util.hpp @@ -186,7 +196,7 @@ test: clean # make asm : compile and generate assembly files. -ASM_CPP_SRCS := main.cpp main-float.cpp $(VARIANT_SRC) +ASM_CPP_SRCS := main.cpp main-float.cpp main-symmetry.cpp main-float_symmetry.cpp $(VARIANT_SRC) ASM_HPP_SRCS := ./headers/util.hpp ASM_SRCS := $(ASM_CPP_SRCS) $(ASM_HPP_SRCS) diff --git a/main-float_symmetry.cpp b/main-float_symmetry.cpp new file mode 100644 index 0000000..7b4b564 --- /dev/null +++ b/main-float_symmetry.cpp @@ -0,0 +1,294 @@ +#include "./headers/board_float.hpp" +#include "./headers/constants_float.hpp" +#include "./headers/pedestrian_float.hpp" +#include "./headers/social_force_float.hpp" + +#include +#include +#include +#include + +#ifndef BENCHMARK +#define BENCHMARK 0 +#endif + +#ifndef ROOFLINE +#define ROOFLINE 0 +#endif + +#ifdef __x86_64__ +#include "./headers/tsc_x86.h" +#include +#define TRASH_BUFFER_SIZE (16ULL * 1024 * 1024 * 2) // 2x L3 (~16MB) on the x86 machine we use for measurements +#define PAGE_SIZE 4096 +#endif + +#ifdef __aarch64__ +#include "./headers/kperf.h" +#include +#define TRASH_BUFFER_SIZE (1502838784ULL * 2) // 2x SLC (~1.5GB) on M3 Pro +#define PAGE_SIZE 16384 +#endif + +#define N_OBSTACLES 2 + +// Benchmark parameters (mostly copied from mvm.c in hw1) +#define WARMUP_ITERS 3 +#define NUM_REPS 15 +#define NUM_RUNS 1 +#define CYCLES_REQUIRED 1e8 +#define MAX_NUM_RUNS (1 << 14) + +// Flushes caches by streaming reads through a large persistent buffer. +long long flush_cache() { + static volatile char *trash_buffer = nullptr; + static bool initialized = false; + + if (!initialized) { + trash_buffer = new volatile char[TRASH_BUFFER_SIZE]; + + // Touch one byte per page once so pages are physically mapped. And we avoid paying the cost of page faults during cache trashing below. + for (size_t i = 0; i < TRASH_BUFFER_SIZE; i += PAGE_SIZE) { + trash_buffer[i] = 1; + } + + initialized = true; + } + + long long acc = 0; + + for (size_t i = 0; i < TRASH_BUFFER_SIZE; i += CACHE_BLOCK_SIZE) { + // Access one byte per cache block is enough to populate the entire block + acc += trash_buffer[i]; + } + +#ifdef __x86_64__ + _mm_lfence(); +#endif + +#ifdef __aarch64__ + __isb(0xF); +#endif + + asm volatile("" ::: "memory"); + + return acc; +} + +int main(int argc, const char *argv[]) { +#ifdef __x86_64__ + // flush denormals to zero via MXCSR. one-time, per thread. + // any subnormal in the j-loop chain stalls fma for 100+ cycles, this kills that. + _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); + _MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_ON); +#endif + + // Check that the correct number of arguments has been passed. + // + // Expected usage: + // + // ./main [csv_output_path] + // + // The optional third argument enables CSV dumping for visualization. + if (argc < 3) { + printf("Usage: ./main [csv_output_path]\n"); + printf("Each iteration takes %.2f seconds in the simulation.\n", dt); + return 1; + } + + // Parse command-line arguments. + int n_p = atoi(argv[1]); + int n_iter = atoi(argv[2]); + + if (n_p <= 0) { + printf("Error: must be a positive integer, got '%s'\n", argv[1]); + return 1; + } + if (n_iter <= 0) { + printf("Error: must be a positive integer, got '%s'\n", argv[2]); + return 1; + } + + // If provided, dump_path is used to write simulation states to CSV. + // In benchmark mode, this path is intentionally ignored to avoid measuring + // file I/O overhead. + const char *dump_path = (argc >= 4) ? argv[3] : nullptr; + +#if BENCHMARK + +#if ROOFLINE + volatile long long trash_value; +#endif + +#ifdef __aarch64__ + // Initialize ARM performance counters. + kperf_init(); + struct performance_counters startperf, endperf; +#endif + + // Warmup runs, to bring caches, predictors, frequency to steady state. These are not measured. + // Each run gets a fresh initialized state because run_simulation mutates the + // pedestrians and board state. + for (int i = 0; i < WARMUP_ITERS; i++) { + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + run_simulation(P, board, n_p, n_iter, nullptr); + delete[] P; + } + + // Calibration: finds num_runs such that one batch runs for at least + // CYCLES_REQUIRED cycles. A long batch keeps makes the timer and other + // noise overhead, negligible relative to the kernel execution cost we're + // actually trying to measure. + // + // Skipped when ROOFLINE is enabled: cold-cache measurements need + // exactly one timed simulation per batch, otherwise only the first + // call starts cold and subsequent calls measure warm-cache behavior. + // We can use large enough inputs so a single call already exceeds CYCLES_REQUIRED, + // when measuring for roofline plot. + // Note: we implemented the calibration exactly as it is done in mvm.c from hw1, we also + // used the same values for NUM_RUNS, etc + int num_runs = NUM_RUNS; +#if !ROOFLINE + while (num_runs < MAX_NUM_RUNS) { + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + + float cycles; + +#ifdef __x86_64__ + myInt64 start = start_tsc(); +#endif + +#ifdef __aarch64__ + startperf = kperf_get_counters(); +#endif + + for (int j = 0; j < num_runs; j++) { + run_simulation(P, board, n_p, n_iter, nullptr); + } + +#ifdef __x86_64__ + cycles = (float)stop_tsc(start); +#endif + +#ifdef __aarch64__ + endperf = kperf_get_counters(); + cycles = (float)(endperf.cycles - startperf.cycles); +#endif + + delete[] P; + + if (cycles >= CYCLES_REQUIRED) + break; + num_runs *= 2; + } +#endif + + // Store the cycle count of each measured repetition. + // The median is then used as the final timing result. + std::vector cycles_v; + cycles_v.reserve(NUM_REPS); + + // Measurement phase + for (int i = 0; i < NUM_REPS; i++) { + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + +#if ROOFLINE + trash_value = flush_cache(); +#endif + +#ifdef __x86_64__ + myInt64 start = start_tsc(); +#endif +#ifdef __aarch64__ + startperf = kperf_get_counters(); +#endif + + for (int j = 0; j < num_runs; j++) { + // Disable CSV dumping during benchmarking to avoid measuring file I/O + // overhead. Initialization is also excluded from the measured region. + + run_simulation(P, board, n_p, n_iter, nullptr); + } + +#ifdef __x86_64__ + float cycles = (float)stop_tsc(start) / num_runs; +#endif +#ifdef __aarch64__ + endperf = kperf_get_counters(); + float cycles = (float)(endperf.cycles - startperf.cycles) / num_runs; +#endif + + delete[] P; + cycles_v.push_back(cycles); + } + + std::sort(cycles_v.begin(), cycles_v.end()); + // Use the median cycle count to reduce the effect of noisy measurements. + float T = cycles_v[NUM_REPS / 2]; + +#else + + // Normal execution mode. If dump_path is provided, the simulation writes a CSV + // file that can later be used to generate a video of the simulation. + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + run_simulation(P, board, n_p, n_iter, dump_path); + + delete[] P; + +#endif + +#if BENCHMARK + + // Flop-count per simulation iteration. + // + // The total work W(n) is estimated from the number of additions, + // multiplications, divisions, square roots, and exponentials performed by the + // simulation kernels. + + // symmetry: each pedestrian-pedestrian pair is computed once instead of twice, + // so the pairwise (n_p - 1) terms get halved -> n_p * (n_p - 1) / 2 pairs total. + // the self-driving force, obstacle force and update pass are untouched. + long long pairs = (long long)n_p * (n_p - 1) / 2; + + long long add_count_per_iter = n_p * (2 + N_OBSTACLES * 10 + 2) + n_p * 7 + pairs * 14; + long long mult_count_per_iter = n_p * (4 + N_OBSTACLES * 13) + n_p * 16 + pairs * 24; + long long div_count_per_iter = n_p * (N_OBSTACLES * 1) + n_p * 2 + pairs * 1; + long long sqrt_count_per_iter = n_p * (N_OBSTACLES * 1) + n_p * 3 + pairs * 3; + long long exp_count_per_iter = n_p * (N_OBSTACLES * 1) + pairs * 1; + + float W_per_iter = add_count_per_iter * cost_add + mult_count_per_iter * cost_mult + div_count_per_iter * cost_div + sqrt_count_per_iter * cost_sqrt + exp_count_per_iter * cost_exp; + + // Estimated memory traffic per iteration. + // TODO: refine this model to account for actual memory accesses, cache reuse, + // and the full interaction loop. + + long long Q_per_iter = sizeof(Pedestrian) * n_p + sizeof(Obstacle) * N_OBSTACLES; + + // Operational intensity: + // + // I(n) = W(n) / Q(n) + // + // where W is estimated work in flops and Q is estimated memory traffic in + // bytes. + + float I = W_per_iter / Q_per_iter; + float W = W_per_iter * n_iter; + + // Performance in estimated flops per cycle. + float P = W / T; + + printf("W(n) per iteration: %.2f flops\n", W_per_iter); + printf("Q(n) per iteration: %lld bytes\n", Q_per_iter); + printf("I(n): %.2f flops/byte\n", I); + printf("W(n): %.2f flops\n", W); + printf("T(n): %.2f cycles\n", T); + printf("P(n): %.2f flops/cycle\n", P); + +#endif + + return 0; +} diff --git a/main-symmetry.cpp b/main-symmetry.cpp new file mode 100644 index 0000000..6725310 --- /dev/null +++ b/main-symmetry.cpp @@ -0,0 +1,287 @@ +#include "./headers/board.hpp" +#include "./headers/constants.hpp" +#include "./headers/pedestrian.hpp" +#include "./headers/social_force.hpp" + +#include +#include +#include +#include + +#ifndef BENCHMARK +#define BENCHMARK 0 +#endif + +#ifndef ROOFLINE +#define ROOFLINE 0 +#endif + +#ifdef __x86_64__ +#include "./headers/tsc_x86.h" +#include +#define TRASH_BUFFER_SIZE (16ULL * 1024 * 1024 * 2) // 2x L3 (~16MB) on the x86 machine we use for measurements +#define PAGE_SIZE 4096 +#endif + +#ifdef __aarch64__ +#include "./headers/kperf.h" +#include +#define TRASH_BUFFER_SIZE (1502838784ULL * 2) // 2x SLC (~1.5GB) on M3 Pro +#define PAGE_SIZE 16384 +#endif + +#define N_OBSTACLES 2 + +// Benchmark parameters (mostly copied from mvm.c in hw1) +#define WARMUP_ITERS 3 +#define NUM_REPS 15 +#define NUM_RUNS 1 +#define CYCLES_REQUIRED 1e8 +#define MAX_NUM_RUNS (1 << 14) + +// Flushes caches by streaming reads through a large persistent buffer. +long long flush_cache() { + static volatile char *trash_buffer = nullptr; + static bool initialized = false; + + if (!initialized) { + trash_buffer = new volatile char[TRASH_BUFFER_SIZE]; + + // Touch one byte per page once so pages are physically mapped. And we avoid paying the cost of page faults during cache trashing below. + for (size_t i = 0; i < TRASH_BUFFER_SIZE; i += PAGE_SIZE) { + trash_buffer[i] = 1; + } + + initialized = true; + } + + long long acc = 0; + + for (size_t i = 0; i < TRASH_BUFFER_SIZE; i += CACHE_BLOCK_SIZE) { + // Access one byte per cache block is enough to populate the entire block + acc += trash_buffer[i]; + } + +#ifdef __x86_64__ + _mm_lfence(); +#endif + +#ifdef __aarch64__ + __isb(0xF); +#endif + + asm volatile("" ::: "memory"); + + return acc; +} + +int main(int argc, const char *argv[]) { + // Check that the correct number of arguments has been passed. + // + // Expected usage: + // + // ./main [csv_output_path] + // + // The optional third argument enables CSV dumping for visualization. + if (argc < 3) { + printf("Usage: ./main [csv_output_path]\n"); + printf("Each iteration takes %.2f seconds in the simulation.\n", dt); + return 1; + } + + // Parse command-line arguments. + int n_p = atoi(argv[1]); + int n_iter = atoi(argv[2]); + + if (n_p <= 0) { + printf("Error: must be a positive integer, got '%s'\n", argv[1]); + return 1; + } + if (n_iter <= 0) { + printf("Error: must be a positive integer, got '%s'\n", argv[2]); + return 1; + } + + // If provided, dump_path is used to write simulation states to CSV. + // In benchmark mode, this path is intentionally ignored to avoid measuring + // file I/O overhead. + const char *dump_path = (argc >= 4) ? argv[3] : nullptr; + +#if BENCHMARK + +#if ROOFLINE + volatile long long trash_value; +#endif + +#ifdef __aarch64__ + // Initialize ARM performance counters. + kperf_init(); + struct performance_counters startperf, endperf; +#endif + + // Warmup runs, to bring caches, predictors, frequency to steady state. These are not measured. + // Each run gets a fresh initialized state because run_simulation mutates the + // pedestrians and board state. + for (int i = 0; i < WARMUP_ITERS; i++) { + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + run_simulation(P, board, n_p, n_iter, nullptr); + delete[] P; + } + + // Calibration: finds num_runs such that one batch runs for at least + // CYCLES_REQUIRED cycles. A long batch keeps makes the timer and other + // noise overhead, negligible relative to the kernel execution cost we're + // actually trying to measure. + // + // Skipped when ROOFLINE is enabled: cold-cache measurements need + // exactly one timed simulation per batch, otherwise only the first + // call starts cold and subsequent calls measure warm-cache behavior. + // We can use large enough inputs so a single call already exceeds CYCLES_REQUIRED, + // when measuring for roofline plot. + // Note: we implemented the calibration exactly as it is done in mvm.c from hw1, we also + // used the same values for NUM_RUNS, etc + int num_runs = NUM_RUNS; +#if !ROOFLINE + while (num_runs < MAX_NUM_RUNS) { + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + + long double cycles; + +#ifdef __x86_64__ + myInt64 start = start_tsc(); +#endif + +#ifdef __aarch64__ + startperf = kperf_get_counters(); +#endif + + for (int j = 0; j < num_runs; j++) { + run_simulation(P, board, n_p, n_iter, nullptr); + } + +#ifdef __x86_64__ + cycles = (long double)stop_tsc(start); +#endif + +#ifdef __aarch64__ + endperf = kperf_get_counters(); + cycles = (long double)(endperf.cycles - startperf.cycles); +#endif + + delete[] P; + + if (cycles >= CYCLES_REQUIRED) + break; + num_runs *= 2; + } +#endif + + // Store the cycle count of each measured repetition. + // The median is then used as the final timing result. + std::vector cycles_v; + cycles_v.reserve(NUM_REPS); + + // Measurement phase + for (int i = 0; i < NUM_REPS; i++) { + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + +#if ROOFLINE + trash_value = flush_cache(); +#endif + +#ifdef __x86_64__ + myInt64 start = start_tsc(); +#endif +#ifdef __aarch64__ + startperf = kperf_get_counters(); +#endif + + for (int j = 0; j < num_runs; j++) { + // Disable CSV dumping during benchmarking to avoid measuring file I/O + // overhead. Initialization is also excluded from the measured region. + + run_simulation(P, board, n_p, n_iter, nullptr); + } + +#ifdef __x86_64__ + long double cycles = (long double)stop_tsc(start) / num_runs; +#endif +#ifdef __aarch64__ + endperf = kperf_get_counters(); + long double cycles = (long double)(endperf.cycles - startperf.cycles) / num_runs; +#endif + + delete[] P; + cycles_v.push_back(cycles); + } + + std::sort(cycles_v.begin(), cycles_v.end()); + // Use the median cycle count to reduce the effect of noisy measurements. + long double T = cycles_v[NUM_REPS / 2]; + +#else + + // Normal execution mode. If dump_path is provided, the simulation writes a CSV + // file that can later be used to generate a video of the simulation. + Pedestrian *P = init_pedestrians(n_p); + Board board = init_board_without_obstacles(); + run_simulation(P, board, n_p, n_iter, dump_path); + + delete[] P; + +#endif + +#if BENCHMARK + + // Flop-count per simulation iteration. + // + // The total work W(n) is estimated from the number of additions, + // multiplications, divisions, square roots, and exponentials performed by the + // simulation kernels. + + // symmetry: each pedestrian-pedestrian pair is computed once instead of twice, + // so the pairwise (n_p - 1) terms get halved -> n_p * (n_p - 1) / 2 pairs total. + // the self-driving force, obstacle force and update pass are untouched. + long long pairs = (long long)n_p * (n_p - 1) / 2; + + long long add_count_per_iter = n_p * (2 + N_OBSTACLES * 10 + 2) + n_p * 7 + pairs * 14; + long long mult_count_per_iter = n_p * (4 + N_OBSTACLES * 13) + n_p * 16 + pairs * 24; + long long div_count_per_iter = n_p * (N_OBSTACLES * 1) + n_p * 2 + pairs * 1; + long long sqrt_count_per_iter = n_p * (N_OBSTACLES * 1) + n_p * 3 + pairs * 3; + long long exp_count_per_iter = n_p * (N_OBSTACLES * 1) + pairs * 1; + + long double W_per_iter = add_count_per_iter * cost_add + mult_count_per_iter * cost_mult + div_count_per_iter * cost_div + sqrt_count_per_iter * cost_sqrt + exp_count_per_iter * cost_exp; + + // Estimated memory traffic per iteration. + // TODO: refine this model to account for actual memory accesses, cache reuse, + // and the full interaction loop. + + long long Q_per_iter = sizeof(Pedestrian) * n_p + sizeof(Obstacle) * N_OBSTACLES; + + // Operational intensity: + // + // I(n) = W(n) / Q(n) + // + // where W is estimated work in flops and Q is estimated memory traffic in + // bytes. + + long double I = W_per_iter / Q_per_iter; + long double W = W_per_iter * n_iter; + + // Performance in estimated flops per cycle. + long double P = W / T; + + printf("W(n) per iteration: %.2Lf flops\n", W_per_iter); + printf("Q(n) per iteration: %lld bytes\n", Q_per_iter); + printf("I(n): %.2Lf flops/byte\n", I); + printf("W(n): %.2Lf flops\n", W); + printf("T(n): %.2Lf cycles\n", T); + printf("P(n): %.2Lf flops/cycle\n", P); + +#endif + + return 0; +} \ No newline at end of file