162 lines
4.5 KiB
Go
162 lines
4.5 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package linalg
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"sourcedock.dev/petrbalvin/tensor/internal/core"
|
||
)
|
||
|
||
// Benchmarks for the minimum degree ordering: the frontier selection
|
||
// against the reference scan beside it, on the mesh patterns the
|
||
// direct solvers are measured on. Both variants run in one binary, and
|
||
// the pair runs in both orders across the two parent benchmarks, so a
|
||
// drift of the machine between the two halves of a round cannot dress
|
||
// itself up as a difference between the variants. The small grid is
|
||
// there so a regression the large grids would drown stays visible.
|
||
|
||
// gridLaplacian3D builds the 7-point Laplacian on a w×h×d grid in
|
||
// row-major order: symmetric positive definite, and the 3-D mesh where
|
||
// an ordering's fill decisions cost the most.
|
||
func gridLaplacian3D(b *testing.B, w, h, d int) *core.SparseCOO {
|
||
b.Helper()
|
||
n := w * h * d
|
||
idx := make([]int64, 0, 7*n)
|
||
vals := make([]float64, 0, 7*n)
|
||
add := func(r, c int, v float64) {
|
||
idx = append(idx, int64(r), int64(c))
|
||
vals = append(vals, v)
|
||
}
|
||
at := func(x, y, z int) int { return (z*h+y)*w + x }
|
||
for z := range d {
|
||
for y := range h {
|
||
for x := range w {
|
||
add(at(x, y, z), at(x, y, z), 6)
|
||
if x+1 < w {
|
||
add(at(x, y, z), at(x+1, y, z), -1)
|
||
add(at(x+1, y, z), at(x, y, z), -1)
|
||
}
|
||
if y+1 < h {
|
||
add(at(x, y, z), at(x, y+1, z), -1)
|
||
add(at(x, y+1, z), at(x, y, z), -1)
|
||
}
|
||
if z+1 < d {
|
||
add(at(x, y, z), at(x, y, z+1), -1)
|
||
add(at(x, y, z+1), at(x, y, z), -1)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return sparseCOOFrom(b, n, idx, vals)
|
||
}
|
||
|
||
// orderingGrid is one mesh the ordering benchmarks run on.
|
||
type orderingGrid struct {
|
||
name string
|
||
w, h, d int
|
||
}
|
||
|
||
// orderingGrids are the meshes: the small grid every regression would
|
||
// show on, the two 2-D meshes, and the 3-D mesh where the quadratic
|
||
// scan costs most.
|
||
var orderingGrids = []orderingGrid{
|
||
{"2d-361", 19, 19, 1},
|
||
{"2d-4096", 64, 64, 1},
|
||
{"2d-16384", 128, 128, 1},
|
||
{"3d-32768", 32, 32, 32},
|
||
}
|
||
|
||
// orderingInput builds one grid's matrix, with the CSC form the
|
||
// orderings consume beside the COO the constructor takes, outside
|
||
// every timed region.
|
||
func orderingInput(b *testing.B, g orderingGrid) (*core.SparseCOO, *SparseCSC) {
|
||
b.Helper()
|
||
var coo *core.SparseCOO
|
||
if g.d == 1 {
|
||
coo = gridLaplacian(b, g.w, g.h)
|
||
} else {
|
||
coo = gridLaplacian3D(b, g.w, g.h, g.d)
|
||
}
|
||
c, err := CSCFromCOO(coo)
|
||
if err != nil {
|
||
b.Fatal(err)
|
||
}
|
||
return coo, c
|
||
}
|
||
|
||
// orderingVariants are the two selections, named for the sub-benchmark
|
||
// that times them: the reference scan and the production frontier.
|
||
func orderingVariants() []struct {
|
||
name string
|
||
run func(*SparseCSC) error
|
||
} {
|
||
return []struct {
|
||
name string
|
||
run func(*SparseCSC) error
|
||
}{
|
||
{"scan", func(c *SparseCSC) error {
|
||
_, err := minimumDegreeScan(c)
|
||
return err
|
||
}},
|
||
{"frontier", func(c *SparseCSC) error {
|
||
_, err := minimumDegree(c)
|
||
return err
|
||
}},
|
||
}
|
||
}
|
||
|
||
// orderingBattery runs the scan and the frontier against the same
|
||
// pattern, in the order the caller picks.
|
||
func orderingBattery(b *testing.B, reverse bool) {
|
||
for _, g := range orderingGrids {
|
||
_, c := orderingInput(b, g)
|
||
variants := orderingVariants()
|
||
if reverse {
|
||
variants[0], variants[1] = variants[1], variants[0]
|
||
}
|
||
for _, v := range variants {
|
||
variant := v
|
||
b.Run(g.name+"/"+variant.name, func(b *testing.B) {
|
||
b.ReportAllocs()
|
||
for b.Loop() {
|
||
if err := variant.run(c); err != nil {
|
||
b.Fatal(err)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// BenchmarkMinimumDegreeOrdering measures the ordering alone, scan
|
||
// first.
|
||
func BenchmarkMinimumDegreeOrdering(b *testing.B) {
|
||
orderingBattery(b, false)
|
||
}
|
||
|
||
// BenchmarkMinimumDegreeOrderingRev measures the ordering alone,
|
||
// frontier first: the mirror of the other parent, so the pair's two
|
||
// halves alternate which variant pays for the position.
|
||
func BenchmarkMinimumDegreeOrderingRev(b *testing.B) {
|
||
orderingBattery(b, true)
|
||
}
|
||
|
||
// BenchmarkSparseCholeskyMinimumDegreeFactor measures NewSparseCholesky
|
||
// end to end with the minimum degree ordering: the ordering sits inside
|
||
// the construction, so its cost is part of the number.
|
||
func BenchmarkSparseCholeskyMinimumDegreeFactor(b *testing.B) {
|
||
for _, g := range orderingGrids {
|
||
coo, _ := orderingInput(b, g)
|
||
b.Run(g.name, func(b *testing.B) {
|
||
b.ReportAllocs()
|
||
for b.Loop() {
|
||
if _, err := NewSparseCholesky(coo, SparseOrderingMinimumDegree); err != nil {
|
||
b.Fatal(err)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|