diff options
| author | y-jan137 <yousefjan24000@gmail.com> | 2026-03-15 11:16:49 +0300 |
|---|---|---|
| committer | y-jan137 <yousefjan24000@gmail.com> | 2026-03-15 11:16:49 +0300 |
| commit | be01607041d2ded41e5bf39a0a8e17ffb9bcc296 (patch) | |
| tree | 12857bb0c9c360fecc23534a1c4185d0b27972e5 /experiments | |
| parent | b11123e1cd710c135d9924f263ab1808cd96c8c2 (diff) | |
Add QR iteration and cleanup
Diffstat (limited to 'experiments')
| -rw-r--r-- | experiments/hilbert_qr.cpp | 17 | ||||
| -rw-r--r-- | experiments/matmul.cpp | 149 | ||||
| -rw-r--r-- | experiments/pivoting_vs_no_pivoting.cpp | 16 |
3 files changed, 149 insertions, 33 deletions
diff --git a/experiments/hilbert_qr.cpp b/experiments/hilbert_qr.cpp index 9209a30..218b35c 100644 --- a/experiments/hilbert_qr.cpp +++ b/experiments/hilbert_qr.cpp @@ -1,9 +1,3 @@ -// Experiment: QR methods on Hilbert matrices -// -// The Hilbert matrix H[i][j] = 1/(i+j+1) is the canonical ill-conditioned -// dense matrix. Its condition number grows roughly as (3.5 * e)^n / sqrt(n), -// reaching ~10^13 at n=10 and ~10^18 at n=14. -// // We compare classical GS, modified GS, and Householder QR on: // - reconstruction error ||A - QR||_F // - orthogonality error ||Q^T Q - I||_F @@ -68,9 +62,6 @@ double orthogonality_error(const QRResult& qr) { return std::sqrt(err); } -// --------------------------------------------------------------------------- -// Timing -// --------------------------------------------------------------------------- using Clock = std::chrono::high_resolution_clock; using Seconds = std::chrono::duration<double>; @@ -88,10 +79,6 @@ double min_time(Fn fn, int trials = 5) { return best; } -// --------------------------------------------------------------------------- -// Run one method on one size, return {recon, ortho, time} or nullopt on failure -// --------------------------------------------------------------------------- - using QRFn = std::function<QRResult(const Matrix&)>; struct Result { double recon, ortho, time_s; }; @@ -110,10 +97,6 @@ std::optional<Result> measure(const Matrix& A, QRFn fn) { } } -// --------------------------------------------------------------------------- -// Pretty printing -// --------------------------------------------------------------------------- - void print_row(const std::string& method, std::optional<Result> r) { std::cout << std::left << std::setw(16) << method; if (!r) { diff --git a/experiments/matmul.cpp b/experiments/matmul.cpp new file mode 100644 index 0000000..8ef0ac5 --- /dev/null +++ b/experiments/matmul.cpp @@ -0,0 +1,149 @@ +#include "linalg_error.hpp" +#include "matrix.hpp" + +#include <chrono> +#include <cstddef> +#include <iomanip> +#include <iostream> +#include <vector> + +#if !defined(LINEAR_ALGEBRA_FORCE_SCALAR_MATMUL) && defined(__AVX512F__) +# define MATMUL_BACKEND "AVX512" +#elif !defined(LINEAR_ALGEBRA_FORCE_SCALAR_MATMUL) && defined(__AVX2__) +# define MATMUL_BACKEND "AVX2" +#elif !defined(LINEAR_ALGEBRA_FORCE_SCALAR_MATMUL) && defined(__AVX__) +# define MATMUL_BACKEND "AVX" +#elif !defined(LINEAR_ALGEBRA_FORCE_SCALAR_MATMUL) && \ + defined(__ARM_NEON) && defined(__aarch64__) && \ + defined(__ARM_FEATURE_FP64_VECTOR_ARITHMETIC) +# define MATMUL_BACKEND "NEON" +#else +# define MATMUL_BACKEND "scalar" +#endif + +using linalg::Matrix; +using Clock = std::chrono::high_resolution_clock; +using Seconds = std::chrono::duration<double>; + + +Matrix naive_matmul(const Matrix& lhs, const Matrix& rhs) { + if (lhs.cols() != rhs.rows()) { + throw linalg::DimensionMismatchError( + "naive_matmul: lhs.cols() != rhs.rows()"); + } + const std::size_t m = lhs.rows(); + const std::size_t n = rhs.cols(); + const std::size_t k = lhs.cols(); + + Matrix result(m, n, 0.0); + + const double* A = lhs.data(); + const double* B = rhs.data(); + double* C = result.data(); + + for (std::size_t i = 0; i < m; ++i) + for (std::size_t j = 0; j < n; ++j) + for (std::size_t p = 0; p < k; ++p) + C[i * n + j] += A[i * k + p] * B[p * n + j]; + + return result; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +// Fill an n×n matrix with a deterministic pattern so the compiler cannot +// optimise multiplications away. +Matrix make_matrix(std::size_t n) { + Matrix M(n, n); + const double inv = 1.0 / static_cast<double>(n + 1); + for (std::size_t i = 0; i < n; ++i) + for (std::size_t j = 0; j < n; ++j) + M(i, j) = static_cast<double>(i + j + 1) * inv; + return M; +} + +template <typename Fn> +double min_time_s(Fn fn, int trials) { + double best = 1e30; + for (int t = 0; t < trials; ++t) { + const auto t0 = Clock::now(); + fn(); + const auto t1 = Clock::now(); + const double elapsed = Seconds(t1 - t0).count(); + if (elapsed < best) best = elapsed; + } + return best; +} + +double flops(std::size_t n) { + const double nd = static_cast<double>(n); + return 2.0 * nd * nd * nd; +} + + +int main() { + const std::vector<std::size_t> sizes = { + 8, 16, 32, 64, 128, 256, 512 + }; + + constexpr std::size_t small_threshold = 128; + constexpr int trials_small = 9; + constexpr int trials_large = 3; + + std::cout << std::string(72, '*') << "\n"; + std::cout << " Matmul benchmark: naive (ijk) vs SIMD (" MATMUL_BACKEND + ") + transpose\n"; + std::cout << " C = A * B, A and B both n×n\n"; + std::cout << std::string(72, '*') << "\n\n"; + + // Column header. + std::cout << std::left + << std::setw(6) << "n" + << std::setw(14) << "naive ms" + << std::setw(14) << "naive GFLOP/s" + << std::setw(14) << "SIMD ms" + << std::setw(14) << "SIMD GFLOP/s" + << std::setw(10) << "speedup" + << "\n"; + std::cout << std::string(72, '-') << "\n"; + + for (const std::size_t n : sizes) { + const Matrix A = make_matrix(n); + const Matrix B = make_matrix(n); + + const int trials = (n <= small_threshold) ? trials_small : trials_large; + + volatile double sink_naive = naive_matmul(A, B)(0, 0); + volatile double sink_simd = (A * B)(0, 0); + (void)sink_naive; + (void)sink_simd; + + const double t_naive = min_time_s([&] { (void)naive_matmul(A, B); }, trials); + const double t_simd = min_time_s([&] { (void)(A * B); }, trials); + + const double fp = flops(n); + const double gf_naive = fp / t_naive / 1e9; + const double gf_simd = fp / t_simd / 1e9; + const double speedup = t_naive / t_simd; + + std::cout << std::left << std::setw(6) << n + << std::fixed << std::setprecision(3) + << std::setw(14) << t_naive * 1e3 + << std::setprecision(2) + << std::setw(14) << gf_naive + << std::setprecision(3) + << std::setw(14) << t_simd * 1e3 + << std::setprecision(2) + << std::setw(14) << gf_simd + << std::setprecision(2) << std::setw(10) << speedup + << "x\n"; + } + + std::cout << "\n(each cell = minimum over " + << trials_small << " trials for n<=" << small_threshold + << ", " << trials_large << " trials for larger n)\n"; + + return 0; +} diff --git a/experiments/pivoting_vs_no_pivoting.cpp b/experiments/pivoting_vs_no_pivoting.cpp index 0e71dc1..57281d8 100644 --- a/experiments/pivoting_vs_no_pivoting.cpp +++ b/experiments/pivoting_vs_no_pivoting.cpp @@ -1,8 +1,3 @@ -// Experiment: partial pivoting vs no-pivot LU -// -// Demonstrates why partial pivoting is essential for numerical stability. -// Run the binary and inspect the residuals printed to stdout. - #include "lu.hpp" #include "matrix.hpp" #include "norms.hpp" @@ -23,7 +18,6 @@ using linalg::Vector; // --------------------------------------------------------------------------- // Local no-pivot LU for comparison only. -// This is intentionally naive — it is here to show what breaks without pivoting. // --------------------------------------------------------------------------- struct NoPivotLU { @@ -55,8 +49,6 @@ NoPivotLU lu_no_pivot(const Matrix& A, double tol = 1e-14) { return NoPivotLU{std::move(L), std::move(U), false, 0}; } -// Solve using a no-pivot LU (L unit lower triangular, U upper triangular). -// If the factorization failed or U is numerically singular, returns nullopt. std::optional<Vector> solve_no_pivot(const NoPivotLU& f, const Vector& b) { if (f.failed) return std::nullopt; try { @@ -178,8 +170,6 @@ void exp_random(std::size_t n = 8) { } // 2. Badly row-scaled matrix -// Rows differ in magnitude by ~10^14. Without pivoting, tiny early pivots -// amplify round-off; with pivoting, the large-row is selected first. void exp_badly_scaled() { const Matrix A{ {1e-14, 1.0, 2.0 }, @@ -191,21 +181,15 @@ void exp_badly_scaled() { } // 3. Classic pathological example for no-pivot LU. -// With epsilon = 1e-15, no-pivot computes a huge multiplier (1/epsilon), -// which causes catastrophic cancellation in the updated rows. -// With pivoting, we swap first and the multiplier is bounded by 1. void exp_epsilon_pathology() { constexpr double eps = 1e-15; const Matrix A{{eps, 1.0}, {1.0, 2.0}}; - // True solution of [eps 1; 1 2] * x = [1+eps; 3] is x = [1; 1]. const Vector b{1.0 + eps, 3.0}; run_case("Epsilon pathology [[1e-15,1],[1,2]] (classic)", A, b); std::cout << " Note: exact solution is x = [1, 1]\n"; } // 4. Matrix where no-pivot LU diverges visibly on a 4x4 example. -// The first pivot is small (0.001) but rows below have entries ~1000. -// No pivot causes multipliers of magnitude 10^6, annihilating subdiagonal info. void exp_amplified_multiplier() { const Matrix A{ {0.001, 1.0, 0.0, 0.0 }, |