diff --git a/.github/workflows/ci-benchmarks.yml b/.github/workflows/ci-benchmarks.yml new file mode 100644 index 0000000..19b863f --- /dev/null +++ b/.github/workflows/ci-benchmarks.yml @@ -0,0 +1,25 @@ +name: Benchmarks + +# Runs the runtime benchmarks in Benchmarks/ and fails if any benchmark allocates more +# than recorded in Benchmarks/Thresholds/ (currently zero for every benchmark). +# Only allocation counts gate CI; timings on shared runners are too noisy to compare. + +on: + push: + branches: [ master, main ] + pull_request: + branches: [ master, main ] + workflow_dispatch: + +jobs: + allocations: + name: Allocation Thresholds + runs-on: ubuntu-latest + container: swift:6.4.1 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Check Allocation Thresholds + working-directory: Benchmarks + run: swift package benchmark thresholds check --metric mallocCountTotal --no-progress diff --git a/Benchmarks/Benchmarks/FirebladeMathBenchmarks/FirebladeMathBenchmarks.swift b/Benchmarks/Benchmarks/FirebladeMathBenchmarks/FirebladeMathBenchmarks.swift new file mode 100644 index 0000000..82a02b6 --- /dev/null +++ b/Benchmarks/Benchmarks/FirebladeMathBenchmarks/FirebladeMathBenchmarks.swift @@ -0,0 +1,142 @@ +import Benchmark +import FirebladeMath + +let benchmarks: @Sendable () -> Void = { + Benchmark.defaultConfiguration = .init( + metrics: [.wallClock, .throughput, .mallocCountTotal], + scalingFactor: .kilo, + maxDuration: .seconds(2), + thresholds: [.mallocCountTotal: .init(absolute: [.p90: 0])] + ) + + let axis = normalize(Vec3f(1, 2, 3)) + let rotation = Quat4f(angle: 0.7, axis: axis) + let transform = Mat4x4f(rotation: 0.3, axis: Vec3f(0, 1, 0)) * Mat4x4f(translation: Vec3f(1, 2, 3)) + + // MARK: - Own implementation on every platform + + // + // Some of these compose FirebladeMath's thin vector/libm wrappers (normalize, cross, sin, ...), + // but the algorithm itself is FirebladeMath's own. + + Benchmark("Mat4x4f.adjugate") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(adjugate(identity(transform))) + } + } + + Benchmark("Mat4x4f.initRotation") { benchmark in + for index in benchmark.scaledIterations { + blackHole(Mat4x4f(rotation: Float(index) * 0.001, axis: identity(axis))) + } + } + + Benchmark("Mat4x4f.lookAt") { benchmark in + for index in benchmark.scaledIterations { + let eye = Vec3f(Float(index) * 0.001, 2, 5) + blackHole(Mat4x4f.look(from: eye, at: identity(Vec3f.zero), up: Vec3f(0, 1, 0))) + } + } + + Benchmark("Mat4x4f.perspectiveRH") { benchmark in + for index in benchmark.scaledIterations { + let aspect = 1.5 + Float(index % 100) * 0.001 + blackHole(Mat4x4f.perspectiveRH(fovy: 1.2, aspect: aspect, zNear: 0.1, zFar: identity(100))) + } + } + + Benchmark("Quat4f.fromEulerAngles123") { benchmark in + for index in benchmark.scaledIterations { + blackHole(Quat4f.fromEulerAngles_123(Vec3f(Float(index) * 0.001, 0.2, 0.3))) + } + } + + Benchmark("Quat4f.toEulerAngles123") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(quaternionToEulerAngles_123(identity(rotation))) + } + } + + Benchmark("Mat4x4f.flatSubscript") { benchmark in + for _ in benchmark.scaledIterations { + let source = identity(transform) + var sum: Float = 0 + for index in 0..<16 { + sum += source[index] + } + blackHole(sum) + } + } + + Benchmark("Quat4f.iteration") { benchmark in + for _ in benchmark.scaledIterations { + var sum: Float = 0 + for component in identity(rotation) { + sum += component + } + blackHole(sum) + } + } + + // MARK: - Own implementation on the portable (non-simd) path + + // + // With the `simd` trait (default on Apple platforms) these forward to Apple's simd library. + // Their numbers describe FirebladeMath's code only on Linux/Windows (CI) or locally with + // FRB_MATH_BENCHMARK_NO_SIMD=1. + + Benchmark("Mat4x4f.initColumns") { benchmark in + let column0 = Vec4f(1, 2, 3, 4) + let column1 = Vec4f(5, 6, 7, 8) + let column2 = Vec4f(9, 10, 11, 12) + let column3 = Vec4f(13, 14, 15, 16) + for index in benchmark.scaledIterations { + let scale = Float(index) + blackHole(Mat4x4f(column0 * scale, column1 * scale, column2 * scale, column3 * scale)) + } + } + + Benchmark("Mat4x4f.multiply") { benchmark in + var matrix = Mat4x4f(diagonal: Vec4f(repeating: 0.5)) + let factor = Mat4x4f(diagonal: Vec4f(repeating: 1.0000001)) + for _ in benchmark.scaledIterations { + matrix = identity(matrix * factor) + } + blackHole(matrix) + } + + Benchmark("Mat4x4f.inverse") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(inverse(identity(transform))) + } + } + + Benchmark("Mat4x4f.fromQuaternion") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(matrix4x4(from: identity(rotation))) + } + } + + Benchmark("Quat4f.fromMatrix") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(quaternion(matrix: identity(transform))) + } + } + + Benchmark("Quat4f.multiply") { benchmark in + var quaternion = rotation + for _ in benchmark.scaledIterations { + quaternion = identity(quaternion * rotation) + } + blackHole(quaternion) + } + + Benchmark("Quat4f.actNormalize") { benchmark in + var vector = Vec3f(1, 2, 3) + let step = Quat4f(angle: 0.001, axis: axis) + for _ in benchmark.scaledIterations { + vector = identity(normalize(act(step, vector))) + } + blackHole(vector) + } +} diff --git a/Benchmarks/Package.resolved b/Benchmarks/Package.resolved new file mode 100644 index 0000000..d4bb23d --- /dev/null +++ b/Benchmarks/Package.resolved @@ -0,0 +1,78 @@ +{ + "originHash" : "4cc28765bde81a339c60ed6b5b6205a92e157503321d0b83b02a98a43733848f", + "pins" : [ + { + "identity" : "benchmark", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ordo-one/benchmark", + "state" : { + "revision" : "def9afdba446b03efbbbcecb3b892e97e8294214", + "version" : "1.38.0" + } + }, + { + "identity" : "hdrhistogram-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/HdrHistogram/hdrhistogram-swift.git", + "state" : { + "revision" : "c2e1210df04b4fff47e53f2f9dad9cc45ae15d63", + "version" : "0.2.0" + } + }, + { + "identity" : "malloc-interposer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ordo-one/malloc-interposer.git", + "state" : { + "revision" : "f479398f73b14dbd1d25f4e01fd667171ddc9f18", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "0442cb5a3f98ab802acb777929fdb446bda11a34", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "869129b7bf4ecc57b97d0193ad29690ca2134750", + "version" : "1.8.1" + } + }, + { + "identity" : "texttable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ordo-one/TextTable.git", + "state" : { + "revision" : "a27a07300cf4ae322e0079ca0a475c5583dd575f", + "version" : "0.0.2" + } + } + ], + "version" : 3 +} diff --git a/Benchmarks/Package.swift b/Benchmarks/Package.swift new file mode 100644 index 0000000..0c97d6a --- /dev/null +++ b/Benchmarks/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 6.3 +// Runtime benchmarks for FirebladeMath, kept in a separate package so that the +// benchmark dependency never becomes part of FirebladeMath's dependency graph. +// +// Run from this directory: +// swift package benchmark +// Benchmark the portable (non-simd) code path on Apple platforms: +// FRB_MATH_BENCHMARK_NO_SIMD=1 swift package benchmark + +import class Foundation.ProcessInfo +import PackageDescription + +let disableSIMD = ProcessInfo.processInfo.environment["FRB_MATH_BENCHMARK_NO_SIMD"] != nil + +let package = Package( + name: "FirebladeMathBenchmarks", + platforms: [.macOS(.v13)], + dependencies: [ + // The package identity of a path dependency is its directory name, i.e. the repository name. + .package(path: "..", traits: disableSIMD ? [] : [.defaults]), + .package(url: "https://github.com/ordo-one/benchmark", .upToNextMajor(from: "1.38.0")) + ], + targets: [ + .executableTarget( + name: "FirebladeMathBenchmarks", + dependencies: [ + .product(name: "FirebladeMath", package: "math"), + .product(name: "Benchmark", package: "benchmark") + ], + path: "Benchmarks/FirebladeMathBenchmarks", + plugins: [ + .plugin(name: "BenchmarkPlugin", package: "benchmark") + ] + ) + ] +) diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.adjugate.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.adjugate.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.adjugate.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.flatSubscript.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.flatSubscript.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.flatSubscript.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.fromQuaternion.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.fromQuaternion.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.fromQuaternion.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.initColumns.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.initColumns.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.initColumns.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.initRotation.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.initRotation.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.initRotation.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.inverse.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.inverse.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.inverse.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.lookAt.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.lookAt.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.lookAt.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.multiply.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.multiply.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.multiply.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.perspectiveRH.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.perspectiveRH.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Mat4x4f.perspectiveRH.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.actNormalize.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.actNormalize.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.actNormalize.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.fromEulerAngles123.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.fromEulerAngles123.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.fromEulerAngles123.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.fromMatrix.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.fromMatrix.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.fromMatrix.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.iteration.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.iteration.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.iteration.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.multiply.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.multiply.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.multiply.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.toEulerAngles123.p90.json b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.toEulerAngles123.p90.json new file mode 100644 index 0000000..9f9de44 --- /dev/null +++ b/Benchmarks/Thresholds/FirebladeMathBenchmarks.Quat4f.toEulerAngles123.p90.json @@ -0,0 +1,3 @@ +{ + "mallocCountTotal" : 0 +} \ No newline at end of file diff --git a/Makefile b/Makefile index 2828757..31de81b 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DOCS_VERSION_PATH ?= main HOSTING_BASE_PATH ?= $(REPO_NAME)/$(DOCS_VERSION_PATH) # Targets -.PHONY: setup lint lint-fix check-inlinable test test-coverage clean pre-commit docs docs-preview docs-generate docs-coverage profile-stats benchmark-commits +.PHONY: setup lint lint-fix check-inlinable test test-coverage clean pre-commit docs docs-preview docs-generate docs-coverage profile-stats benchmark-commits benchmark benchmark-thresholds-check benchmark-thresholds-update setup: @echo "Detected Package Swift Version: $(PACKAGE_SWIFT_VERSION)" @@ -87,3 +87,12 @@ benchmark-commits: chmod +x Scripts/profile-compiler-stats.sh chmod +x Scripts/benchmark-commits.py python3 Scripts/benchmark-commits.py + +benchmark: + cd Benchmarks && swift package benchmark + +benchmark-thresholds-check: + cd Benchmarks && swift package benchmark thresholds check --metric mallocCountTotal + +benchmark-thresholds-update: + cd Benchmarks && swift package --allow-writing-to-package-directory benchmark thresholds update --metric mallocCountTotal