/** * Benchmarking different implementations of Vasicek model path * generation. Includes serial loop, OpenMP parallel loop, and * vectorized version using ndarray operations. * * Requirements: * - GSL * - OpenMP * - narray-c * * Compile with: * gcc -O3 -fopenmp -lm -lgsl -lndarray -o bench_vasicek bench_vasicek.c * * Run with: * ./bench_vasicek * * (c) 2026 Jaime Lopez */ #define _POSIX_C_SOURCE 199309L #include #include #include #include #include #include #include typedef struct { double mean_reversion_rate; // kappa double long_term_mean; // theta double volatility; // sigma } VasicekParams; typedef struct { size_t num_steps; size_t num_paths; } SimulationParams; NDArray paths_vasicek_loop_serial(const VasicekParams* par, double initial_rate, double maturity, const SimulationParams* sim) { const double dt = maturity / sim->num_steps; NDArray rates = ndarray_new(NDA_DIMS(sim->num_steps, sim->num_paths)); ndarray_fill_slice(rates, 0, // axis 0, // index initial_rate); gsl_rng* rng = gsl_rng_alloc(gsl_rng_default); for (size_t path = 0; path < sim->num_paths; ++path) { for (size_t step = 1; step < sim->num_steps; ++step) { double curr = ndarray_get(rates, NDA_POS(step - 1, path)); double term1 = par->mean_reversion_rate * (par->long_term_mean - curr) * dt; double term2 = par->volatility * sqrt(dt) * gsl_ran_gaussian(rng, 1.0); double next = curr + term1 + term2; ndarray_set(rates, NDA_POS(step, path), next); } } gsl_rng_free(rng); return rates; } NDArray paths_vasicek_vectorized(const VasicekParams* par, double initial_rate, double maturity, const SimulationParams* sim) { const double dt = maturity / sim->num_steps; NDArray rates = ndarray_new(NDA_DIMS(sim->num_steps, sim->num_paths)); ndarray_fill_slice(rates, 0, // axis 0, // index initial_rate); NDArray t1 = ndarray_new(NDA_DIMS(1, sim->num_paths)); NDArray t2 = ndarray_new(NDA_DIMS(1, sim->num_paths)); for (size_t step = 1; step < sim->num_steps; ++step) { /* drift term: κ(θ - r)dt */ ndarray_copy_slice(t2, 0, 0, rates, 0, step - 1); ndarray_add_scalar(t2, -par->long_term_mean); ndarray_mul_scalar(t2, -par->mean_reversion_rate * dt); /* diffusion term: σ√dt Z */ NDArray rannorm = ndarray_new_randnorm(NDA_DIMS(1, sim->num_paths), 0.0, 1.0); ndarray_mul_scalar(rannorm, par->volatility * sqrt(dt)); ndarray_add(t2, rannorm); ndarray_free(rannorm); /* r_new = r_old + dr */ ndarray_copy_slice(t1, 0, 0, rates, 0, step - 1); ndarray_add(t1, t2); /* update step */ ndarray_copy_slice(rates, 0, step, t1, 0, 0); } ndarray_free_all(NDA_LIST(t1, t2)); return rates; } NDArray paths_vasicek_loop(const VasicekParams* par, double initial_rate, double maturity, const SimulationParams* sim) { const double dt = maturity / sim->num_steps; NDArray rates = ndarray_new(NDA_DIMS(sim->num_steps, sim->num_paths)); ndarray_fill_slice(rates, 0, // axis 0, // index initial_rate); int num_threads = omp_get_max_threads(); gsl_rng** rng_array = (gsl_rng**)malloc(num_threads * sizeof(gsl_rng*)); #pragma omp parallel { int thread_id = omp_get_thread_num(); rng_array[thread_id] = gsl_rng_alloc(gsl_rng_default); gsl_rng_set(rng_array[thread_id], 42 + thread_id); } #pragma omp parallel for collapse(2) schedule(dynamic) for (size_t path = 0; path < sim->num_paths; ++path) { for (size_t step = 1; step < sim->num_steps; ++step) { int thread_id = omp_get_thread_num(); gsl_rng* rng = rng_array[thread_id]; double curr = ndarray_get(rates, NDA_POS(step - 1, path)); double term1 = par->mean_reversion_rate * (par->long_term_mean - curr) * dt; double term2 = par->volatility * sqrt(dt) * gsl_ran_gaussian(rng, 1.0); double next = curr + term1 + term2; ndarray_set(rates, NDA_POS(step, path), next); } } #pragma omp parallel { int thread_id = omp_get_thread_num(); gsl_rng_free(rng_array[thread_id]); } free(rng_array); return rates; } static long long get_time_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000000000LL + ts.tv_nsec; } int main(void) { VasicekParams params = { .mean_reversion_rate = 0.15, .long_term_mean = 0.05, .volatility = 0.02 }; double initial_rate = 0.03; double maturity = 10.0; int iterations = 10; struct { size_t steps; size_t paths; const char* desc; } configs[] = { { 100, 100, "Small" }, { 250, 500, "Medium" }, { 500, 1000, "Large" }, { 1000, 2000, "Very Large" }, { 2500, 5000, "Huge" }, { 5000, 10000, "Extreme" }, { 10000, 20000, "Ultra Extreme" } }; for (int i = 0; i < 7; ++i) { SimulationParams sim = { .num_steps = configs[i].steps, .num_paths = configs[i].paths }; printf("%s\n", configs[i].desc); long long total_serial = 0; for (int j = 0; j < iterations; ++j) { long long start = get_time_ns(); NDArray result = paths_vasicek_loop_serial(¶ms, initial_rate, maturity, &sim); long long end = get_time_ns(); total_serial += (end - start); ndarray_free(result); } double time_serial = total_serial / 1e6; printf(" Serial: %.2f ms/iter\n", time_serial / iterations); long long total_omp = 0; for (int j = 0; j < iterations; ++j) { long long start = get_time_ns(); NDArray result = paths_vasicek_loop(¶ms, initial_rate, maturity, &sim); long long end = get_time_ns(); total_omp += (end - start); ndarray_free(result); } double time_omp = total_omp / 1e6; printf(" OpenMP: %.2f ms/iter\n", time_omp / iterations); long long total_vec = 0; for (int j = 0; j < iterations; ++j) { long long start = get_time_ns(); NDArray result = paths_vasicek_vectorized(¶ms, initial_rate, maturity, &sim); long long end = get_time_ns(); total_vec += (end - start); ndarray_free(result); } double time_vec = total_vec / 1e6; printf(" Vectorized: %.2f ms/iter\n", time_vec / iterations); printf(" Speedups: Vec/Serial=%.2fx, OpenMP/Serial=%.2fx\n\n", time_serial / time_vec, time_serial / time_omp); } return 0; }