Repository: google/vector_math.dart
Pull request #345: Improve performance of functions with dynamic arguments
Author: login: osa1
Created: 2025-04-16T11:36:36Z | Merged: 2025-05-01T15:57:07Z

`Matrix4.translate` is used by Flutter and it often appears as an overhead when
profiling Flutter apps compiled to Wasm.

![matrix4_translate_prof](https://github.com/user-attachments/assets/546c09f0-2c09-437f-9cb8-c4bbc20117d3)

The function currently takes a `dynamic` argument and takes different code
paths based on the type.

This is inefficient when the call site already knows the argument's type.

In general, when we have a performance-critical generic function that checks
types of the arguments, it makes sense to introduce specialized versions of the
function based on precise argument types that it handles, so that callers can
call the more efficient specialized versions and avoid the type test overheads.

For each function with `dynamic` argument, this PR introduces specialized
functions based on precise argument types that the functions handle, call those
specialized functions from the `dynamic` functions, and then inline the
`dynamic` functions.

This allows compilers to eliminate the type tests when the types are known in a
call site, and call the efficient functions directly.

For example, for `Vector4.translate`, we introduce:

- `Vector4.translateByDouble(double x, double y, double z)`
- `Vector4.translateByVector3(Vector3 v3)`
- `Vector4.translateByVector4(Vector4 v4)`

Call sites that know the argument type can directly call these for performance.

Existing call sites will be automatically improved when updating the library,
as the type-testing functions will be inlined and type tests will be eliminated
in most (probably all cases).

dart2wasm benchmarks: (`-O2`)

```
// Before
Matrix4.translateByDoubleGeneric(RunTime): 8.032 us.
Matrix4.translateByVector3Generic(RunTime): 8.457468284493933 us.
Matrix4.translateByVector4Generic(RunTime): 9.094468169361408 us.

// After
Matrix4.translateByDoubleGeneric(RunTime): 4.86 us.
Matrix4.translateByVector3Generic(RunTime): 4.796994003757495 us.
Matrix4.translateByVector4Generic(RunTime): 4.795 us.
Matrix4.translateByDouble(RunTime): 4.8425 us.
Matrix4.translateByVector3(RunTime): 4.997243753445308 us.
Matrix4.translateByVector4(RunTime): 4.909493863132671 us.
```

dart2js benchmarks: (`-O4`)

```
// Before
Matrix4.translateByDoubleGeneric(RunTime): 6.224801171860612 us.
Matrix4.translateByVector3Generic(RunTime): 9.400219449286789 us.
Matrix4.translateByVector4Generic(RunTime): 10.751954304194207 us.

// After
Matrix4.translateByDoubleGeneric(RunTime): 4.21 us.
Matrix4.translateByVector3Generic(RunTime): 4.388117869308463 us.
Matrix4.translateByVector4Generic(RunTime): 4.9375 us.
Matrix4.translateByDouble(RunTime): 4.1782447771940285 us.
Matrix4.translateByVector3(RunTime): 4.436994453756933 us.
Matrix4.translateByVector4(RunTime): 5.0375 us.
```

AOT benchmarks:

```
// Before
Matrix4.translateByDoubleGeneric(RunTime): 6.5326339347321305 us.
Matrix4.translateByVector3Generic(RunTime): 6.445286109427781 us.
Matrix4.translateByVector4Generic(RunTime): 6.807083684061711 us.

// After
Matrix4.translateByDoubleGeneric(RunTime): 4.193214 us.
Matrix4.translateByVector3Generic(RunTime): 4.132444 us.
Matrix4.translateByVector4Generic(RunTime): 4.821352357295285 us.
Matrix4.translateByDouble(RunTime): 4.074234 us.
Matrix4.translateByVector3(RunTime): 4.061202 us.
Matrix4.translateByVector4(RunTime): 4.735463330670837 us.
```

JIT benchmarks:

```
// Before
Matrix4.translateByDoubleGeneric(RunTime): 4.189242 us.
Matrix4.translateByVector3Generic(RunTime): 8.532402136592522 us.
Matrix4.translateByVector4Generic(RunTime): 7.937426969102983 us.

// After
Matrix4.translateByDoubleGeneric(RunTime): 4.029674 us.
Matrix4.translateByVector3Generic(RunTime): 4.037351666666667 us.
Matrix4.translateByVector4Generic(RunTime): 4.901740372824534 us.
Matrix4.translateByDouble(RunTime): 4.247654940431325 us.
Matrix4.translateByVector3(RunTime): 4.058694 us.
Matrix4.translateByVector4(RunTime): 4.831515710605362 us.
```

Performance or other improved functions `scale`, `scaled`, `operator *`, and
`leftTranslate` should be improved similarly.


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fc07278..398ef8f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,30 @@
+## 2.2.0-wip
+
+- Performance of functions that take `dynamic` arguments improved.
+- Most of the members that take `dynamic` arguments are deprecated. New members
+  with non-`dynamic` arguments added.
+
+  Deprecated members:
+  - `Matrix4.translate`
+
+    Instead use: `Matrix4.translateByDouble`, `Matrix4.translateByVector3`,
+    `Matrix4.translateByVector4`.
+
+  - `Matrix4.leftTranslate`
+
+    Instead use: `Matrix4.leftTranslateByDouble`,
+    `Matrix4.leftTranslateByVector3`, `Matrix4.leftTranslateByVector4`.
+
+  - `Matrix4.scale`
+
+    Instead use: `Matrix4.scaleByDouble`, `Matrix4.scaleByVector3`,
+    `Matrix4.scaleByVector4`.
+
+  - `Matrix4.scaled`
+
+    Instead use: `Matrix4.scaledByDouble`, `Matrix4.scaledByVector3`,
+    `Matrix4.scaledByVector4`.
+
 ## 2.1.5
 
 - Fixed `operator -()` of Quaternion (Contributed by tlserver)
diff --git a/benchmark/matrix_bench.dart b/benchmark/matrix_bench.dart
index 7d0932e0..3cc7ddd6 100644
--- a/benchmark/matrix_bench.dart
+++ b/benchmark/matrix_bench.dart
@@ -2,6 +2,8 @@
 // All rights reserved. Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// ignore_for_file: deprecated_member_use_from_same_package
+
 import 'dart:math' as math;
 import 'dart:typed_data';
 
@@ -321,6 +323,148 @@ class Matrix3TransposeMultiplyBenchmark extends BenchmarkBase {
   }
 }
 
+class Matrix4TranslateByDoubleGenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByDoubleGenericBenchmark()
+      : super('Matrix4.translateByDoubleGeneric');
+
+  final temp = Matrix4.zero()..setIdentity();
+
+  static void main() {
+    Matrix4TranslateByDoubleGenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(10.0, 20.0, 30.0);
+    }
+  }
+}
+
+class Matrix4TranslateByVector3GenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByVector3GenericBenchmark()
+      : super('Matrix4.translateByVector3Generic');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector3(10.0, 20.0, 30.0);
+
+  static void main() {
+    Matrix4TranslateByVector3GenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByVector4GenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByVector4GenericBenchmark()
+      : super('Matrix4.translateByVector4Generic');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector4(10.0, 20.0, 30.0, 40.0);
+
+  static void main() {
+    Matrix4TranslateByVector4GenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByDoubleBenchmark extends BenchmarkBase {
+  Matrix4TranslateByDoubleBenchmark() : super('Matrix4.translateByDouble');
+
+  final temp = Matrix4.zero()..setIdentity();
+
+  static void main() {
+    Matrix4TranslateByDoubleBenchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByDouble(
+          i.toDouble(), (i * 10).toDouble(), (i * 5).toDouble(), 1.0);
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByDouble(10.0, 20.0, 30.0, 1.0);
+    }
+  }
+}
+
+class Matrix4TranslateByVector3Benchmark extends BenchmarkBase {
+  Matrix4TranslateByVector3Benchmark() : super('Matrix4.translateByVector3');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector3(10.0, 20.0, 30.0);
+
+  static void main() {
+    Matrix4TranslateByVector3Benchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByVector3(
+          Vector3(i.toDouble(), (i * 10).toDouble(), (i * 5).toDouble()));
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByVector3(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByVector4Benchmark extends BenchmarkBase {
+  Matrix4TranslateByVector4Benchmark() : super('Matrix4.translateByVector4');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector4(10.0, 20.0, 30.0, 40.0);
+
+  static void main() {
+    Matrix4TranslateByVector4Benchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByVector4(Vector4(i.toDouble(), (i * 10).toDouble(),
+          (i * 5).toDouble(), (i * 20).toDouble()));
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByVector4(vec);
+    }
+  }
+}
+
 void main() {
   MatrixMultiplyBenchmark.main();
   SIMDMatrixMultiplyBenchmark.main();
@@ -335,4 +479,10 @@ void main() {
   Matrix3TransformVector3Benchmark.main();
   Matrix3TransformVector2Benchmark.main();
   Matrix3TransposeMultiplyBenchmark.main();
+  Matrix4TranslateByDoubleGenericBenchmark.main();
+  Matrix4TranslateByVector3GenericBenchmark.main();
+  Matrix4TranslateByVector4GenericBenchmark.main();
+  Matrix4TranslateByDoubleBenchmark.main();
+  Matrix4TranslateByVector3Benchmark.main();
+  Matrix4TranslateByVector4Benchmark.main();
 }
diff --git a/lib/src/vector_math/matrix2.dart b/lib/src/vector_math/matrix2.dart
index 50bb87f1..cd102bdc 100644
--- a/lib/src/vector_math/matrix2.dart
+++ b/lib/src/vector_math/matrix2.dart
@@ -215,17 +215,21 @@ class Matrix2 {
   }
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final Object result;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector2) {
-      return transformed(arg);
-    }
-    if (arg is Matrix2) {
-      return multiplied(arg);
+      result = scaled(arg);
+    } else if (arg is Vector2) {
+      result = transformed(arg);
+    } else if (arg is Matrix2) {
+      result = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return result;
   }
 
   /// Returns new matrix after component wise this + [arg]
diff --git a/lib/src/vector_math/matrix3.dart b/lib/src/vector_math/matrix3.dart
index cea8dd0f..c61208b1 100644
--- a/lib/src/vector_math/matrix3.dart
+++ b/lib/src/vector_math/matrix3.dart
@@ -326,17 +326,21 @@ class Matrix3 {
   }
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final Object result;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector3) {
-      return transformed(arg);
-    }
-    if (arg is Matrix3) {
-      return multiplied(arg);
+      result = scaled(arg);
+    } else if (arg is Vector3) {
+      result = transformed(arg);
+    } else if (arg is Matrix3) {
+      result = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return result;
   }
 
   /// Returns new matrix after component wise this + [arg]
diff --git a/lib/src/vector_math/matrix4.dart b/lib/src/vector_math/matrix4.dart
index c87081fb..8c82523c 100644
--- a/lib/src/vector_math/matrix4.dart
+++ b/lib/src/vector_math/matrix4.dart
@@ -461,7 +461,7 @@ class Matrix4 {
   void setFromTranslationRotationScale(
       Vector3 translation, Quaternion rotation, Vector3 scale) {
     setFromTranslationRotation(translation, rotation);
-    this.scale(scale);
+    scaleByVector3(scale);
   }
 
   /// Sets the upper 2x2 of the matrix to be [arg].
@@ -638,20 +638,29 @@ class Matrix4 {
   Matrix4 operator -() => clone()..negate();
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  ///
+  /// [arg] should be a [double] (to scale), [Vector4] (to transform), [Vector3]
+  /// (to transform), or [Matrix4] (to multiply).
+  ///
+  /// If you know the argument type in a call site, prefer [scaledByDouble],
+  /// [transformed], [transformed3], or [multiplied] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final Object result;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector4) {
-      return transformed(arg);
-    }
-    if (arg is Vector3) {
-      return transformed3(arg);
-    }
-    if (arg is Matrix4) {
-      return multiplied(arg);
+      result = scaledByDouble(arg, arg, arg, 1.0);
+    } else if (arg is Vector4) {
+      result = transformed(arg);
+    } else if (arg is Vector3) {
+      result = transformed3(arg);
+    } else if (arg is Matrix4) {
+      result = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return result;
   }
 
   /// Returns new matrix after component wise this + [arg]
@@ -660,97 +669,144 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use translateByVector3, translateByVector4, '
+      'or translateByDouble instead')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      translateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z, w.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByDouble(double tx, double ty, double tz, double tw) {
     final t1 = _m4storage[0] * tx +
         _m4storage[4] * ty +
         _m4storage[8] * tz +
         _m4storage[12] * tw;
+    _m4storage[12] = t1;
+
     final t2 = _m4storage[1] * tx +
         _m4storage[5] * ty +
         _m4storage[9] * tz +
         _m4storage[13] * tw;
+    _m4storage[13] = t2;
+
     final t3 = _m4storage[2] * tx +
         _m4storage[6] * ty +
         _m4storage[10] * tz +
         _m4storage[14] * tw;
+    _m4storage[14] = t3;
+
     final t4 = _m4storage[3] * tx +
         _m4storage[7] * ty +
         _m4storage[11] * tz +
         _m4storage[15] * tw;
-    _m4storage[12] = t1;
-    _m4storage[13] = t2;
-    _m4storage[14] = t3;
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector3(Vector3 v3) =>
+      translateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Translate this matrix by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector4(Vector4 v4) =>
+      translateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Multiply this by a translation from the left.
-  /// The translation can be specified with a  [Vector3], [Vector4], or x, y, z.
+  ///
+  /// The translation can be specified with a [Vector3], [Vector4], or x, y, z
+  /// as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer
+  /// [leftTranslateByDouble], [leftTranslateByVector3], or
+  /// [leftTranslateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use leftTranslateByVector3, leftTranslateByVector4, '
+      'or leftTranslateByDouble instead')
   void leftTranslate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      leftTranslateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void leftTranslateByDouble(double tx, double ty, double tz, double tw) {
     // Column 1
-    _m4storage[0] += tx * _m4storage[3];
-    _m4storage[1] += ty * _m4storage[3];
-    _m4storage[2] += tz * _m4storage[3];
-    _m4storage[3] = tw * _m4storage[3];
+    final r1 = _m4storage[3];
+    _m4storage[0] += tx * r1;
+    _m4storage[1] += ty * r1;
+    _m4storage[2] += tz * r1;
+    _m4storage[3] = tw * r1;
 
     // Column 2
-    _m4storage[4] += tx * _m4storage[7];
-    _m4storage[5] += ty * _m4storage[7];
-    _m4storage[6] += tz * _m4storage[7];
-    _m4storage[7] = tw * _m4storage[7];
+    final r2 = _m4storage[7];
+    _m4storage[4] += tx * r2;
+    _m4storage[5] += ty * r2;
+    _m4storage[6] += tz * r2;
+    _m4storage[7] = tw * r2;
 
     // Column 3
-    _m4storage[8] += tx * _m4storage[11];
-    _m4storage[9] += ty * _m4storage[11];
-    _m4storage[10] += tz * _m4storage[11];
-    _m4storage[11] = tw * _m4storage[11];
+    final r3 = _m4storage[11];
+    _m4storage[8] += tx * r3;
+    _m4storage[9] += ty * r3;
+    _m4storage[10] += tz * r3;
+    _m4storage[11] = tw * r3;
 
     // Column 4
-    _m4storage[12] += tx * _m4storage[15];
-    _m4storage[13] += ty * _m4storage[15];
-    _m4storage[14] += tz * _m4storage[15];
-    _m4storage[15] = tw * _m4storage[15];
+    final r4 = _m4storage[15];
+    _m4storage[12] += tx * r4;
+    _m4storage[13] += ty * r4;
+    _m4storage[14] += tz * r4;
+    _m4storage[15] = tw * r4;
   }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void leftTranslateByVector3(Vector3 v3) =>
+      leftTranslateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void leftTranslateByVector4(Vector4 v4) =>
+      leftTranslateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Rotate this [angle] radians around [axis]
   void rotate(Vector3 axis, double angle) {
     final len = axis.length;
@@ -864,27 +920,31 @@ class Matrix4 {
     _m4storage[7] = t8;
   }
 
-  /// Scale this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Scale this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [scaleByDouble],
+  /// [scaleByVector3], or [scaleByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use scaleByVector3, scaleByVector4, or scaleByDouble instead')
   void scale(dynamic x, [double? y, double? z]) {
-    double sx;
-    double sy;
-    double sz;
-    final sw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      sx = x.x;
-      sy = x.y;
-      sz = x.z;
+      scaleByVector3(x);
     } else if (x is Vector4) {
-      sx = x.x;
-      sy = x.y;
-      sz = x.z;
+      scaleByVector4(x);
     } else if (x is double) {
-      sx = x;
-      sy = y ?? x;
-      sz = z ?? x;
+      scaleByDouble(x, y ?? x, z ?? x, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Scale this matrix.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void scaleByDouble(double sx, double sy, double sz, double sw) {
     _m4storage[0] *= sx;
     _m4storage[1] *= sx;
     _m4storage[2] *= sx;
@@ -903,10 +963,45 @@ class Matrix4 {
     _m4storage[15] *= sw;
   }
 
+  /// Scale this matrix.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void scaleByVector3(Vector3 v3) => scaleByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Scale this matrix.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void scaleByVector4(Vector4 v4) => scaleByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Create a copy of this scaled by a [Vector3], [Vector4] or [x],[y], and
   /// [z].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use scaledByVector3, scaledByVector4, or scaledByDouble instead')
   Matrix4 scaled(dynamic x, [double? y, double? z]) => clone()..scale(x, y, z);
 
+  /// Create a copy of this scaled by [x], [y], [z], and [t].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  Matrix4 scaledByDouble(double x, double y, double z, double t) =>
+      clone()..scaleByDouble(x, y, z, t);
+
+  /// Create a copy of this scaled by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  Matrix4 scaledByVector3(Vector3 v3) => clone()..scaleByVector3(v3);
+
+  /// Create a copy of this scaled by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  Matrix4 scaledByVector4(Vector4 v4) => clone()..scaleByVector4(v4);
+
   /// Zeros this.
   void setZero() {
     _m4storage[0] = 0.0;
diff --git a/lib/src/vector_math/opengl.dart b/lib/src/vector_math/opengl.dart
index 935bc1a5..74475f89 100644
--- a/lib/src/vector_math/opengl.dart
+++ b/lib/src/vector_math/opengl.dart
@@ -278,7 +278,7 @@ Matrix4 makePlaneProjection(Vector3 planeNormal, Vector3 planePoint) {
 Matrix4 makePlaneReflection(Vector3 planeNormal, Vector3 planePoint) {
   final v = Vector4(planeNormal.storage[0], planeNormal.storage[1],
       planeNormal.storage[2], 0.0);
-  final outer = Matrix4.outer(v, v)..scale(2.0);
+  final outer = Matrix4.outer(v, v)..scaleByDouble(2.0, 2.0, 2.0, 1.0);
   var r = Matrix4.zero();
   r = r - outer;
   final scale = 2.0 * planePoint.dot(planeNormal);
diff --git a/lib/src/vector_math_64/matrix2.dart b/lib/src/vector_math_64/matrix2.dart
index e8f3b40c..a35be282 100644
--- a/lib/src/vector_math_64/matrix2.dart
+++ b/lib/src/vector_math_64/matrix2.dart
@@ -215,17 +215,21 @@ class Matrix2 {
   }
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final Object result;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector2) {
-      return transformed(arg);
-    }
-    if (arg is Matrix2) {
-      return multiplied(arg);
+      result = scaled(arg);
+    } else if (arg is Vector2) {
+      result = transformed(arg);
+    } else if (arg is Matrix2) {
+      result = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return result;
   }
 
   /// Returns new matrix after component wise this + [arg]
diff --git a/lib/src/vector_math_64/matrix3.dart b/lib/src/vector_math_64/matrix3.dart
index 89892d33..e5638f9d 100644
--- a/lib/src/vector_math_64/matrix3.dart
+++ b/lib/src/vector_math_64/matrix3.dart
@@ -326,17 +326,21 @@ class Matrix3 {
   }
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final Object result;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector3) {
-      return transformed(arg);
-    }
-    if (arg is Matrix3) {
-      return multiplied(arg);
+      result = scaled(arg);
+    } else if (arg is Vector3) {
+      result = transformed(arg);
+    } else if (arg is Matrix3) {
+      result = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return result;
   }
 
   /// Returns new matrix after component wise this + [arg]
diff --git a/lib/src/vector_math_64/matrix4.dart b/lib/src/vector_math_64/matrix4.dart
index 90a2e8cb..f1ed9f00 100644
--- a/lib/src/vector_math_64/matrix4.dart
+++ b/lib/src/vector_math_64/matrix4.dart
@@ -461,7 +461,7 @@ class Matrix4 {
   void setFromTranslationRotationScale(
       Vector3 translation, Quaternion rotation, Vector3 scale) {
     setFromTranslationRotation(translation, rotation);
-    this.scale(scale);
+    scaleByVector3(scale);
   }
 
   /// Sets the upper 2x2 of the matrix to be [arg].
@@ -638,20 +638,29 @@ class Matrix4 {
   Matrix4 operator -() => clone()..negate();
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  ///
+  /// [arg] should be a [double] (to scale), [Vector4] (to transform), [Vector3]
+  /// (to transform), or [Matrix4] (to multiply).
+  ///
+  /// If you know the argument type in a call site, prefer [scaledByDouble],
+  /// [transformed], [transformed3], or [multiplied] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final Object result;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector4) {
-      return transformed(arg);
-    }
-    if (arg is Vector3) {
-      return transformed3(arg);
-    }
-    if (arg is Matrix4) {
-      return multiplied(arg);
+      result = scaledByDouble(arg, arg, arg, 1.0);
+    } else if (arg is Vector4) {
+      result = transformed(arg);
+    } else if (arg is Vector3) {
+      result = transformed3(arg);
+    } else if (arg is Matrix4) {
+      result = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return result;
   }
 
   /// Returns new matrix after component wise this + [arg]
@@ -660,97 +669,144 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use translateByVector3, translateByVector4, '
+      'or translateByDouble instead')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      translateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z, w.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByDouble(double tx, double ty, double tz, double tw) {
     final t1 = _m4storage[0] * tx +
         _m4storage[4] * ty +
         _m4storage[8] * tz +
         _m4storage[12] * tw;
+    _m4storage[12] = t1;
+
     final t2 = _m4storage[1] * tx +
         _m4storage[5] * ty +
         _m4storage[9] * tz +
         _m4storage[13] * tw;
+    _m4storage[13] = t2;
+
     final t3 = _m4storage[2] * tx +
         _m4storage[6] * ty +
         _m4storage[10] * tz +
         _m4storage[14] * tw;
+    _m4storage[14] = t3;
+
     final t4 = _m4storage[3] * tx +
         _m4storage[7] * ty +
         _m4storage[11] * tz +
         _m4storage[15] * tw;
-    _m4storage[12] = t1;
-    _m4storage[13] = t2;
-    _m4storage[14] = t3;
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector3(Vector3 v3) =>
+      translateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Translate this matrix by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector4(Vector4 v4) =>
+      translateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Multiply this by a translation from the left.
-  /// The translation can be specified with a  [Vector3], [Vector4], or x, y, z.
+  ///
+  /// The translation can be specified with a [Vector3], [Vector4], or x, y, z
+  /// as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer
+  /// [leftTranslateByDouble], [leftTranslateByVector3], or
+  /// [leftTranslateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use leftTranslateByVector3, leftTranslateByVector4, '
+      'or leftTranslateByDouble instead')
   void leftTranslate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      leftTranslateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void leftTranslateByDouble(double tx, double ty, double tz, double tw) {
     // Column 1
-    _m4storage[0] += tx * _m4storage[3];
-    _m4storage[1] += ty * _m4storage[3];
-    _m4storage[2] += tz * _m4storage[3];
-    _m4storage[3] = tw * _m4storage[3];
+    final r1 = _m4storage[3];
+    _m4storage[0] += tx * r1;
+    _m4storage[1] += ty * r1;
+    _m4storage[2] += tz * r1;
+    _m4storage[3] = tw * r1;
 
     // Column 2
-    _m4storage[4] += tx * _m4storage[7];
-    _m4storage[5] += ty * _m4storage[7];
-    _m4storage[6] += tz * _m4storage[7];
-    _m4storage[7] = tw * _m4storage[7];
+    final r2 = _m4storage[7];
+    _m4storage[4] += tx * r2;
+    _m4storage[5] += ty * r2;
+    _m4storage[6] += tz * r2;
+    _m4storage[7] = tw * r2;
 
     // Column 3
-    _m4storage[8] += tx * _m4storage[11];
-    _m4storage[9] += ty * _m4storage[11];
-    _m4storage[10] += tz * _m4storage[11];
-    _m4storage[11] = tw * _m4storage[11];
+    final r3 = _m4storage[11];
+    _m4storage[8] += tx * r3;
+    _m4storage[9] += ty * r3;
+    _m4storage[10] += tz * r3;
+    _m4storage[11] = tw * r3;
 
     // Column 4
-    _m4storage[12] += tx * _m4storage[15];
-    _m4storage[13] += ty * _m4storage[15];
-    _m4storage[14] += tz * _m4storage[15];
-    _m4storage[15] = tw * _m4storage[15];
+    final r4 = _m4storage[15];
+    _m4storage[12] += tx * r4;
+    _m4storage[13] += ty * r4;
+    _m4storage[14] += tz * r4;
+    _m4storage[15] = tw * r4;
   }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void leftTranslateByVector3(Vector3 v3) =>
+      leftTranslateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void leftTranslateByVector4(Vector4 v4) =>
+      leftTranslateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Rotate this [angle] radians around [axis]
   void rotate(Vector3 axis, double angle) {
     final len = axis.length;
@@ -864,27 +920,31 @@ class Matrix4 {
     _m4storage[7] = t8;
   }
 
-  /// Scale this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Scale this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [scaleByDouble],
+  /// [scaleByVector3], or [scaleByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use scaleByVector3, scaleByVector4, or scaleByDouble instead')
   void scale(dynamic x, [double? y, double? z]) {
-    double sx;
-    double sy;
-    double sz;
-    final sw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      sx = x.x;
-      sy = x.y;
-      sz = x.z;
+      scaleByVector3(x);
     } else if (x is Vector4) {
-      sx = x.x;
-      sy = x.y;
-      sz = x.z;
+      scaleByVector4(x);
     } else if (x is double) {
-      sx = x;
-      sy = y ?? x;
-      sz = z ?? x;
+      scaleByDouble(x, y ?? x, z ?? x, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Scale this matrix.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void scaleByDouble(double sx, double sy, double sz, double sw) {
     _m4storage[0] *= sx;
     _m4storage[1] *= sx;
     _m4storage[2] *= sx;
@@ -903,10 +963,45 @@ class Matrix4 {
     _m4storage[15] *= sw;
   }
 
+  /// Scale this matrix.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void scaleByVector3(Vector3 v3) => scaleByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Scale this matrix.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void scaleByVector4(Vector4 v4) => scaleByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Create a copy of this scaled by a [Vector3], [Vector4] or [x],[y], and
   /// [z].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use scaledByVector3, scaledByVector4, or scaledByDouble instead')
   Matrix4 scaled(dynamic x, [double? y, double? z]) => clone()..scale(x, y, z);
 
+  /// Create a copy of this scaled by [x], [y], [z], and [t].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  Matrix4 scaledByDouble(double x, double y, double z, double t) =>
+      clone()..scaleByDouble(x, y, z, t);
+
+  /// Create a copy of this scaled by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  Matrix4 scaledByVector3(Vector3 v3) => clone()..scaleByVector3(v3);
+
+  /// Create a copy of this scaled by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  Matrix4 scaledByVector4(Vector4 v4) => clone()..scaleByVector4(v4);
+
   /// Zeros this.
   void setZero() {
     _m4storage[0] = 0.0;
diff --git a/lib/src/vector_math_64/opengl.dart b/lib/src/vector_math_64/opengl.dart
index 7d7de934..6d25d363 100644
--- a/lib/src/vector_math_64/opengl.dart
+++ b/lib/src/vector_math_64/opengl.dart
@@ -278,7 +278,7 @@ Matrix4 makePlaneProjection(Vector3 planeNormal, Vector3 planePoint) {
 Matrix4 makePlaneReflection(Vector3 planeNormal, Vector3 planePoint) {
   final v = Vector4(planeNormal.storage[0], planeNormal.storage[1],
       planeNormal.storage[2], 0.0);
-  final outer = Matrix4.outer(v, v)..scale(2.0);
+  final outer = Matrix4.outer(v, v)..scaleByDouble(2.0, 2.0, 2.0, 1.0);
   var r = Matrix4.zero();
   r = r - outer;
   final scale = 2.0 * planePoint.dot(planeNormal);
diff --git a/pubspec.yaml b/pubspec.yaml
index 69f40984..162d06c0 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: vector_math
-version: 2.1.5
+version: 2.2.0-wip
 description: A Vector Math library for 2D and 3D applications.
 repository: https://github.com/google/vector_math.dart
 
diff --git a/test/geometry_test.dart b/test/geometry_test.dart
index 78336cea..6c81de58 100644
--- a/test/geometry_test.dart
+++ b/test/geometry_test.dart
@@ -2,6 +2,8 @@
 // All rights reserved. Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// ignore_for_file: deprecated_member_use_from_same_package
+
 import 'dart:typed_data';
 
 import 'package:test/test.dart';
diff --git a/test/matrix4_test.dart b/test/matrix4_test.dart
index e338752a..6900e5ca 100644
--- a/test/matrix4_test.dart
+++ b/test/matrix4_test.dart
@@ -2,6 +2,8 @@
 // All rights reserved. Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// ignore_for_file: deprecated_member_use_from_same_package
+
 import 'dart:math' as math;
 import 'dart:typed_data';
 
@@ -372,6 +374,36 @@ void testMatrix4Translation() {
   for (var i = 0; i < inputA.length; i++) {
     relativeTest(output1[i], output2[i]);
   }
+
+  final input = Matrix4.fromList([
+    1, 5, 9, 13, //
+    2, 6, 10, 14, //
+    3, 7, 11, 15, //
+    4, 8, 12, 16, //
+  ]);
+  final output3 = input.clone();
+  output3[12] = input.dotRow(0, Vector4(4, 8, 12, 1));
+  output3[13] = input.dotRow(1, Vector4(4, 8, 12, 1));
+  output3[14] = input.dotRow(2, Vector4(4, 8, 12, 1));
+  output3[15] = input.dotRow(3, Vector4(4, 8, 12, 1));
+  relativeTest(
+    input.clone()..translateByDouble(4.0, 8.0, 12.0, 1.0),
+    output3,
+  );
+  relativeTest(
+    input.clone()..translateByVector3(Vector3(4.0, 8.0, 12.0)),
+    output3,
+  );
+
+  final output4 = input.clone();
+  output4[12] = input.dotRow(0, Vector4(4, 8, 12, 16));
+  output4[13] = input.dotRow(1, Vector4(4, 8, 12, 16));
+  output4[14] = input.dotRow(2, Vector4(4, 8, 12, 16));
+  output4[15] = input.dotRow(3, Vector4(4, 8, 12, 16));
+  relativeTest(
+    input.clone()..translateByVector4(Vector4(4, 8, 12, 16)),
+    output4,
+  );
 }
 
 void testMatrix4Scale() {


Reviews:
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T21:02:26Z
user: login: osa1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T10:36:12Z
user: login: mkustermann
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-28T19:49:23Z
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T20:32:29Z
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T20:47:35Z
user: login: mraleph
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T08:32:50Z
user: login: osa1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T10:48:09Z
user: login: osa1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T11:43:59Z
user: login: mkustermann
body: Generally LGTM with the following comments
* move stores up as far as possible
* access `[15]` first
* have single implementation that all others delegate to
* make functions only have a single return & mark as `@pragma('dart2js:prefer-inline')`

If the last point improves performance in dart2js similar to dart2wasm & vm, then do we get meaningful perf benefits from having the `{translate,scale,...}By{Double,...}` operations?
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-30T11:31:17Z
user: login: kevmoo
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T14:38:11Z
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T15:31:32Z
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:01:08Z
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:29:32Z
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T16:39:41Z
user: login: osa1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T09:11:13Z
user: login: osa1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T09:19:15Z
user: login: osa1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T10:04:36Z
user: login: kevmoo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T22:52:49Z
user: login: kevmoo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T22:58:56Z
user: login: rakudrama
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T23:14:48Z
user: login: kevmoo
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T00:48:59Z

Review comments:
diff_hunk: @@ -703,6 +707,63 @@ class Matrix4 {
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
path: lib/src/vector_math/matrix4.dart
user: login: rakudrama
body: To help the compiler eliminate bounds checks, make sure `_m4storage[15]` is at the top.
Some methods access in reverse to achieve this, but I think it is easier to simply add the statement

    _m4storage[15]; // bounds check limit

Without this hint, the compiler needs to bounds check at least 0, 4, 8, 12, 13, 14, 15.

created_at: 2025-04-25T21:01:19Z
author_association: COLLABORATOR
diff_hunk: @@ -703,6 +707,63 @@ class Matrix4 {
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
path: lib/src/vector_math/matrix4.dart
user: login: osa1
body: It would be great if this worked across all targets to drop bounds checks in the generated code but in my testing I only see small very small improvements in dart2js, and even that may just be noise.

In dart2wasm, wasm-opt just drops the array access, probably due to `--traps-never-happen`.

In AOT and JIT the difference is very small, probably just noise.

(Note: TFA doesn't drop the array access, but I haven't checked if VM does it before code generation)

Interestingly, if I refactor the function to access `_m4storage[15]` as the first thing:

```dart
void translateByDouble(double tx, double ty, double tz) {
  final t4 = _m4storage[15] +
      _m4storage[11] * tz +
      _m4storage[7] * ty +
      _m4storage[3] * tx;
  final t3 = _m4storage[14] +
      _m4storage[10] * tz +
      _m4storage[6] * ty +
      _m4storage[2] * tx;
  final t2 = _m4storage[13] +
      _m4storage[9] * tz +
      _m4storage[5] * ty +
      _m4storage[1] * tx;
  final t1 = _m4storage[12] +
      _m4storage[8] * tz +
      _m4storage[4] * ty +
      _m4storage[0] * tx;
  _m4storage[15] = t4;
  _m4storage[14] = t3;
  _m4storage[13] = t2;
  _m4storage[12] = t1;
}
```

I get worse numbers on all targets.

I also notice that some of the other functions in this library also have the same optimization opportunity. For example `leftTranslate`:

```dart
    // Column 1
    _m4storage[0] += tx * _m4storage[3];
    _m4storage[1] += ty * _m4storage[3];
    _m4storage[2] += tz * _m4storage[3];
    _m4storage[3] = tw * _m4storage[3];

    // Column 2
    _m4storage[4] += tx * _m4storage[7];
    _m4storage[5] += ty * _m4storage[7];
    _m4storage[6] += tz * _m4storage[7];
    _m4storage[7] = tw * _m4storage[7];

    // Column 3
    _m4storage[8] += tx * _m4storage[11];
    _m4storage[9] += ty * _m4storage[11];
    _m4storage[10] += tz * _m4storage[11];
    _m4storage[11] = tw * _m4storage[11];

    // Column 4
    _m4storage[12] += tx * _m4storage[15];
    _m4storage[13] += ty * _m4storage[15];
    _m4storage[14] += tz * _m4storage[15];
    _m4storage[15] = tw * _m4storage[15];
```

But since I'm not seeing any difference with this change I'm not sure if it's worth it.
created_at: 2025-04-28T10:36:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -703,6 +707,63 @@ class Matrix4 {
     _m4storage[15] = t4;
path: lib/src/vector_math/matrix4.dart
user: login: mkustermann
body: Can this method delegate to `translateByX` methods below?
created_at: 2025-04-28T19:25:39Z
author_association: NONE
diff_hunk: @@ -661,10 +661,14 @@ class Matrix4 {
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
   /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @Deprecated('Use the other specialized translate methods instead.')
path: lib/src/vector_math/matrix4.dart
user: login: mkustermann
body: This pattern of using `dynamic` and type checking also exists in other methods. If we change one of them it would make sense to be consistent and handle all of them the same way: `operator*`, `leftTranslate`, `scale`, `scaled`, ... possibly more in `matrix4.dart`, possibly more in other files.

Have you tried an experiment where we change the body of this function to be like this
```dart
@pragma('wasm:prefer-inline')
@pragma('vm:prefer-inline')
@pragma('dart2js:tryInline') // (or whatever it's called for dart2js) 
void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
    if (x is Vector3) {
      _translateVector3(...);
      return;
    }
   if (x is Vector4) {
     _translateVector4(...);
     return;
   }
 ...
```
and see what happens to perf?

I'd think in almost all cases the call site knows the type of `x` and inlining the function will probably get rid of all the type checks and only keeps one body. In dart2wasm I don't know if binaryen would be able to remove it. Possibly we could make an optimization in the inliner that strengthens the type of (final) parameters based on call site type information)

The benefit would be: We avoid creating more API functions and thereby avoid requiring our ecosystem to migrate their code and (figers crossed) get basically the same result as this change.
created_at: 2025-04-28T19:42:26Z
author_association: NONE
diff_hunk: @@ -661,10 +661,14 @@ class Matrix4 {
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
   /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @Deprecated('Use the other specialized translate methods instead.')
path: lib/src/vector_math/matrix4.dart
user: login: rakudrama
body: dart2js won't inline that function even with the pragma - there are some edge cases for multiple `return` statements, so that inlining is declined. We could probably make it work for statement-context calls.
Both `@pragma('dart2js:tryInline')` and `@pragma('dart2js:prefer-inline')` are supported and mean the same thing.

Adding an annotation that is ignored would be harmless, but I would still want the type-safe API.

created_at: 2025-04-28T20:32:29Z
author_association: COLLABORATOR
diff_hunk: @@ -703,6 +707,63 @@ class Matrix4 {
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
path: lib/src/vector_math/matrix4.dart
user: login: rakudrama
body: For native and perhaps wasm, I would expect the bounds checks to affect code size more than performance.
At the machine level, the length would be CSEed and enregistered (`cmp r,#N; ja L;`) the branch predicted.
It should make no difference on dart2js at `-O4` since that removes the checks.

> In dart2wasm, wasm-opt just drops the array access, probably due to `--traps-never-happen`.

At `-O2`?
created_at: 2025-04-28T20:47:35Z
author_association: COLLABORATOR
diff_hunk: @@ -703,6 +707,63 @@ class Matrix4 {
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
path: lib/src/vector_math/matrix4.dart
user: login: mraleph
body: > For native and perhaps wasm, I would expect the bounds checks to affect code size more than performance.

FWIW bounds checks cause significant code quality issues for VM because they use pinned registers for both index and the length - even when lengths is constant (for the sake of outlining the slow path into a single stub). But this is usually only noticeable in very tight loops where register allocation quality is important.
created_at: 2025-04-29T08:32:50Z
author_association: MEMBER
diff_hunk: @@ -703,6 +707,63 @@ class Matrix4 {
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
path: lib/src/vector_math/matrix4.dart
user: login: osa1
body: > For native and perhaps wasm, I would expect the bounds checks to affect code size more than performance

Good point, I checked binary sizes. Unfortunately the problem is wasm-opt isn't eliminating any bounds checks regardless of the indexing order. And dart2wasm doesn't do that kind of thing at all, we rely on wasm-opt.

I reported the issue to binaryen devs. In short, there's another optimization step missing in wasm-opt before it will be able to eliminate the bounds checks when we index the largest index first.

With VM it made 0 byte difference to the benchmark .exe (6,702,056 bytes before and after) when I changed indexing to index the highest index first (with the diff above).

> > In dart2wasm, wasm-opt just drops the array access, probably due to --traps-never-happen.
>
> At -O2?

Yes, wasm-opt flags don't change with the optimization levels, only dart2wasm flags change.
created_at: 2025-04-29T10:48:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -661,10 +661,14 @@ class Matrix4 {
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
   /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @Deprecated('Use the other specialized translate methods instead.')
path: lib/src/vector_math/matrix4.dart
user: login: osa1
body: I tried this and it improved the `dynamic` function performance quite a bit, but still not to the level of typed functions.

I'm a bit puzzled as to how wasm-opt is able to optimize this though, because in kernel (after TFA) we still see a `dynamic` argument:

```
method run() → void {
  for (core::int i = 0; i.{core::num::<}(100){(core::num) → core::bool}; i = i.{core::num::+}(1){(core::num) → core::int}) {
    this.{mat::Matrix4TranslateByDoubleGenericBenchmark::temp}{vec::Matrix4}.{vec::Matrix4::translate}(10.0, 20.0, 30.0){(dynamic, [core::double, core::double]) → void};
  }
}
```

Before wasm-opt:

```
(func $Matrix4TranslateByDoubleGenericBenchmark.run (;709;) (param $var0 (ref $BenchmarkBase)) (result (ref null $#Top))
  (local $var1 (ref $Matrix4TranslateByDoubleGenericBenchmark))
  (local $var2 i64)
  (local $var3 (ref $Matrix4))
  (local $var4 (ref null $#Top))
  (local $var5 (ref null $BoxedDouble))
  (local $var6 (ref null $BoxedDouble))
  local.get $var0
  ref.cast $Matrix4TranslateByDoubleGenericBenchmark
  local.set $var1
  i64.const 0
  local.set $var2
  block $label0
    loop $label2
      ...
      block $label1 (result (ref null $#Top))
        local.get $var4
        call $<obj> is Vector3
        if
          local.get $var3
          local.get $var4
          ref.cast $Vector3
          call $Matrix4.translateByVector3
          br $label1
        else
          local.get $var4
          call $<obj> is Vector4
          if
            local.get $var3
            local.get $var4
            ref.cast $Vector4
            call $Matrix4.translateByVector4
            br $label1
          else
            local.get $var4
            call $<obj> is double
            if
              local.get $var3
              local.get $var4
              ref.cast $BoxedDouble
              struct.get $BoxedDouble $field1
              local.get $var5
              struct.get $BoxedDouble $field1
              local.get $var6
              struct.get $BoxedDouble $field1
              call $Matrix4.translateByDouble
              br $label1
            else
              call $UnimplementedError
              call $Error._throwWithCurrentStackTrace
              unreachable
            end
          end
        end
        ref.null none
      end $label1
      drop
      local.get $var2
      i64.const 1
      i64.add
      local.set $var2
      br $label2
    end $label2
  end $label0
  ref.null none
)
```

wasm-opt is somehow able to drop the code for type tests other than `<obj> is double`:

```
(func $Matrix4TranslateByDoubleGenericBenchmark.run (;372;) (param $var0 (ref $BenchmarkBase)) (result (ref null $#Top))
  (local $var1 i64)
  (local $var2 (ref $Matrix4TranslateByDoubleGenericBenchmark_113))
  (local $var3 (ref $Matrix4_156))
  local.get $var0
  ref.cast $Matrix4TranslateByDoubleGenericBenchmark_113
  local.set $var2
  loop $label0
    local.get $var1
    i64.const 100
    i64.lt_s
    if
      local.get $var2
      struct.get $Matrix4TranslateByDoubleGenericBenchmark_113 $field3
      local.set $var3
      global.get $global68
      call $<obj> is Vector3
      drop
      global.get $global68
      call $<obj> is Vector4
      drop
      global.get $global68
      call $<obj> is double
      if
        local.get $var3
        f64.const 10
        f64.const 20
        f64.const 30
        call $Matrix4.translateByDouble
      else
        call $UnimplementedError
        call $Error._throwWithCurrentStackTrace
        unreachable
      end
      local.get $var1
      i64.const 1
      i64.add
      local.set $var1
      br $label0
    end
  end $label0
  ref.null none
)
```

Clearly it figures out that `<obj> is Vector3` never holds (and same for `Vector4`), and it sees that `<obj> is double` can hold, but it can't see that it always hold. No idea how it can tell that the argument is not `Vector3` and `Vector4` without realizing that's it's `double`.

It also improves VM performance a little bit.

dart2wasm before:

```
Matrix4.translateByDoubleGeneric(RunTime): 8.69121740793472 us.
Matrix4.translateByVector3Generic(RunTime): 13.893333333333333 us.
Matrix4.translateByVector4Generic(RunTime): 16.95736434108527 us.
Matrix4.translateByDouble(RunTime): 7.636 us.
Matrix4.translateByVector3(RunTime): 9.130468043361848 us.
Matrix4.translateByVector4(RunTime): 9.10097042184613 us.
```


dart2wasm after:

```
Matrix4.translateByDoubleGeneric(RunTime): 7.514990606261742 us.
Matrix4.translateByVector3Generic(RunTime): 8.855969004108486 us.
Matrix4.translateByVector4Generic(RunTime): 9.094468169361408 us.
Matrix4.translateByDouble(RunTime): 8.261969017616185 us.
Matrix4.translateByVector3(RunTime): 8.788469240357658 us.
Matrix4.translateByVector4(RunTime): 9.062970545345728 us.
```

VM before:

```
Matrix4.translateByDoubleGeneric(RunTime): 4.591483760645299 us.
Matrix4.translateByVector3Generic(RunTime): 6.791057970119567 us.
Matrix4.translateByVector4Generic(RunTime): 7.213656965857585 us.
Matrix4.translateByDouble(RunTime): 4.492614 us.
Matrix4.translateByVector3(RunTime): 5.866527266945466 us.
Matrix4.translateByVector4(RunTime): 6.080305359465621 us.
```

VM after:

```
Matrix4.translateByDoubleGeneric(RunTime): 5.2590825 us.
Matrix4.translateByVector3Generic(RunTime): 5.819971565049761 us.
Matrix4.translateByVector4Generic(RunTime): 5.99830050297412 us.
Matrix4.translateByDouble(RunTime): 4.425238218452227 us.
Matrix4.translateByVector3(RunTime): 5.9266411467177065 us.
Matrix4.translateByVector4(RunTime): 6.152720694558611 us.
```

> dart2js won't inline that function even with the pragma

I still tried this just in case, and indeed it doesn't make any difference.

---

Based on this, I updated `translate` to call the typed API and added inline pragmas for the Wasm and VM. I left the typed versions for dart2js, and also it's still a good idea to use them whenever possible.
created_at: 2025-04-29T11:43:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -215,6 +215,8 @@ class Matrix2 {
   }
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
   dynamic operator *(dynamic arg) {
     if (arg is double) {
       return scaled(arg);
path: lib/src/vector_math/matrix2.dart
user: login: mkustermann
body: From Stephens comments it seems like dart2js won't inline functions with multiple return values. But we can change these functions to only have a single return, allowing dart2js to inline them, no?

```dart
dynamic result;
if (...) {
  result = ...;
} else if (...) {
  result = ...;
} else {
  throw ...
},
return result;
```
and add `@pragma('dart2js:prefer-inline')`

That may also improve the benchmarks using old API methods in dart2js similar to dart2wasm&vm

(also other places)
created_at: 2025-04-30T11:02:48Z
author_association: NONE
diff_hunk: @@ -660,27 +668,81 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      return translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      return translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      return translateByDouble(x, y, z);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
+    final t1 = _m4storage[0] * tx +
+        _m4storage[4] * ty +
+        _m4storage[8] * tz +
+        _m4storage[12];
+    final t2 = _m4storage[1] * tx +
+        _m4storage[5] * ty +
+        _m4storage[9] * tz +
+        _m4storage[13];
+    final t3 = _m4storage[2] * tx +
+        _m4storage[6] * ty +
+        _m4storage[10] * tz +
+        _m4storage[14];
+    final t4 = _m4storage[3] * tx +
+        _m4storage[7] * ty +
+        _m4storage[11] * tz +
+        _m4storage[15];
+    _m4storage[12] = t1;
path: lib/src/vector_math/matrix4.dart
user: login: mkustermann
body: Consider moving the store up after `final t1`, then the compiler doesn't have to keep `t1` alive until down (I suspect our optimizer will not move the store up as high as it can)

(also other places)
created_at: 2025-04-30T11:08:26Z
author_association: NONE
diff_hunk: @@ -703,6 +707,63 @@ class Matrix4 {
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
path: lib/src/vector_math/matrix4.dart
user: login: mkustermann
body: Consider addressing below comment I left about moving the stores up and then reverse `t1...t4` so `_m4storage[15]` is accessed first (as Stephen & Slava said, this should remove all the other bounds checks)

(also other places)
created_at: 2025-04-30T11:13:29Z
author_association: NONE
diff_hunk: @@ -704,27 +766,83 @@ class Matrix4 {
   }
 
   /// Multiply this by a translation from the left.
-  /// The translation can be specified with a  [Vector3], [Vector4], or x, y, z.
+  ///
+  /// The translation can be specified with a [Vector3], [Vector4], or x, y, z
+  /// as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer
+  /// [leftTranslateByDouble], [leftTranslateByVector3], or
+  /// [leftTranslateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
   void leftTranslate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      leftTranslateByDouble(x, y, z);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Multiply this by a translation from the left.
+  void leftTranslateByDouble(double tx, double ty, double tz) {
+    // Column 1
+    _m4storage[0] += tx * _m4storage[3];
path: lib/src/vector_math/matrix4.dart
user: login: mkustermann
body: Consider hoisting the `_m4storage[3]` into a variable as some compilers may not perform this optimization

Also consider moving the `_m4storage[15]` operation first.

(also in other cases)
created_at: 2025-04-30T11:19:46Z
author_association: NONE
diff_hunk: @@ -864,27 +982,65 @@ class Matrix4 {
     _m4storage[7] = t8;
   }
 
-  /// Scale this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Scale this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [scaleByDouble],
+  /// [scaleByVector3], or [scaleByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
path: lib/src/vector_math/matrix4.dart
user: login: mkustermann
body: This function doesn't have multiple returns in it, so dart2js would inline it. Why not add `@pragma('dart2js:prefer-inline')` here as well?

(As mentioned in other comment, I think we should convert all functions to have only a single return, allowing dart2js optimizing all of them)
created_at: 2025-04-30T11:21:56Z
author_association: NONE
diff_hunk: @@ -660,27 +668,81 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      return translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      return translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      return translateByDouble(x, y, z);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
+    final t1 = _m4storage[0] * tx +
+        _m4storage[4] * ty +
+        _m4storage[8] * tz +
+        _m4storage[12];
+    final t2 = _m4storage[1] * tx +
+        _m4storage[5] * ty +
+        _m4storage[9] * tz +
+        _m4storage[13];
+    final t3 = _m4storage[2] * tx +
+        _m4storage[6] * ty +
+        _m4storage[10] * tz +
+        _m4storage[14];
+    final t4 = _m4storage[3] * tx +
+        _m4storage[7] * ty +
+        _m4storage[11] * tz +
+        _m4storage[15];
+    _m4storage[12] = t1;
+    _m4storage[13] = t2;
+    _m4storage[14] = t3;
+    _m4storage[15] = t4;
+  }
+
+  /// Translate this matrix by a [Vector3].
+  void translateByVector3(Vector3 v3) {
+    final tx = v3.x;
path: lib/src/vector_math/matrix4.dart
user: login: mkustermann
body: Can we simply:
```dart
@pragma('vm:prefer-inline')
@pragma('dart2wasm:prefer-inline')
@pragma('dart2js:prefer-inline')
void translateByVector3(Vector3 v3)
    => translateByDouble(v3.x, v3.y, v3.z);
```
?

Also in other cases: Can we structure the code in such a way that we have the implementation only once and inline in such a way that we get the equivalent as in this CL?
created_at: 2025-04-30T11:25:48Z
author_association: NONE
diff_hunk: @@ -321,6 +321,148 @@ class Matrix3TransposeMultiplyBenchmark extends BenchmarkBase {
   }
 }
 
+class Matrix4TranslateByDoubleGenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByDoubleGenericBenchmark()
+      : super('Matrix4.translateByDoubleGeneric');
+
+  final temp = Matrix4.zero()..setIdentity();
+
+  static void main() {
+    Matrix4TranslateByDoubleGenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(10.0, 20.0, 30.0);
+    }
+  }
+}
+
+class Matrix4TranslateByVector3GenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByVector3GenericBenchmark()
+      : super('Matrix4.translateByVector3Generic');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector3(10.0, 20.0, 30.0);
+
+  static void main() {
+    Matrix4TranslateByVector3GenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByVector4GenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByVector4GenericBenchmark()
+      : super('Matrix4.translateByVector4Generic');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector4(10.0, 20.0, 30.0, 40.0);
+
+  static void main() {
+    Matrix4TranslateByVector4GenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByDoubleBenchmark extends BenchmarkBase {
+  Matrix4TranslateByDoubleBenchmark() : super('Matrix4.translateByDouble');
+
+  final temp = Matrix4.zero()..setIdentity();
+
+  static void main() {
+    Matrix4TranslateByDoubleBenchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByDouble(
+          i.toDouble(), (i * 10).toDouble(), (i * 5).toDouble(), 1.0);
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByDouble(10.0, 20.0, 30.0, 1.0);
+    }
+  }
+}
+
+class Matrix4TranslateByVector3Benchmark extends BenchmarkBase {
+  Matrix4TranslateByVector3Benchmark() : super('Matrix4.translateByVector3');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector3(10.0, 20.0, 30.0);
+
+  static void main() {
+    Matrix4TranslateByVector3Benchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByVector3(
+          Vector3(i.toDouble(), (i * 10).toDouble(), (i * 5).toDouble()));
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByVector3(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByVector4Benchmark extends BenchmarkBase {
+  Matrix4TranslateByVector4Benchmark() : super('Matrix4.translateByVector4');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector4(10.0, 20.0, 30.0, 40.0);
+
+  static void main() {
+    Matrix4TranslateByVector4Benchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByVector4(Vector4(i.toDouble(), (i * 10).toDouble(),
+          (i * 5).toDouble(), (i * 20).toDouble()));
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByVector4(vec);
+    }
+  }
+}
+
 void main() {
   MatrixMultiplyBenchmark.main();
path: benchmark/matrix_bench.dart
user: login: rakudrama
body: This structure of running each benchmark in turn is susceptible to JIT-effects where the code is initially specialized to the first benchmark, and then de-optimized and recompiled against less constrained inputs.
When this happens, the reported performance is dependent on the order of the benchmarks,
To counter this, I usually create a list of benchmarks, warm them all up, then do the actual measurements.
created_at: 2025-04-30T15:31:32Z
author_association: COLLABORATOR
diff_hunk: @@ -660,27 +668,81 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      return translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      return translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      return translateByDouble(x, y, z);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z.
+  void translateByDouble(double tx, double ty, double tz) {
+    final t1 = _m4storage[0] * tx +
+        _m4storage[4] * ty +
+        _m4storage[8] * tz +
+        _m4storage[12];
+    final t2 = _m4storage[1] * tx +
+        _m4storage[5] * ty +
+        _m4storage[9] * tz +
+        _m4storage[13];
+    final t3 = _m4storage[2] * tx +
+        _m4storage[6] * ty +
+        _m4storage[10] * tz +
+        _m4storage[14];
+    final t4 = _m4storage[3] * tx +
+        _m4storage[7] * ty +
+        _m4storage[11] * tz +
+        _m4storage[15];
+    _m4storage[12] = t1;
path: lib/src/vector_math/matrix4.dart
user: login: rakudrama
body: In general I would be very careful moving writes since some of these classes can have constructors where 'storage' that is aliased via a typed data view.
Here I think it would be safe since there is only one array.
(A lot of methods are actually incorrect in the aliasing case, but don't break more of them.)

created_at: 2025-04-30T16:01:08Z
author_association: COLLABORATOR
diff_hunk: @@ -638,20 +638,29 @@ class Matrix4 {
   Matrix4 operator -() => clone()..negate();
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  ///
+  /// [arg] should be a [double] (to scale), [Vector4] (to transform), [Vector3]
+  /// (to transform), or [Matrix4] (to multiply).
+  ///
+  /// If you know the argument type in a call site, prefer [scaled],
+  /// [transformed], [transformed3], or [multiplied] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final dynamic value;
path: lib/src/vector_math/matrix4.dart
user: login: rakudrama
body: Two nits:
1. I prefer Martin's suggestion of `result` - the reader already knows what the variable is for.
2. Prefer `Object` over `dynamic` for the variable - it signals that you don't intend to use the result via a dynamic call.
created_at: 2025-04-30T16:29:32Z
author_association: COLLABORATOR
diff_hunk: @@ -638,20 +638,29 @@ class Matrix4 {
   Matrix4 operator -() => clone()..negate();
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  ///
+  /// [arg] should be a [double] (to scale), [Vector4] (to transform), [Vector3]
+  /// (to transform), or [Matrix4] (to multiply).
+  ///
+  /// If you know the argument type in a call site, prefer [scaled],
+  /// [transformed], [transformed3], or [multiplied] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final dynamic value;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector4) {
-      return transformed(arg);
-    }
-    if (arg is Vector3) {
-      return transformed3(arg);
-    }
-    if (arg is Matrix4) {
-      return multiplied(arg);
+      value = scaled(arg);
+    } else if (arg is Vector4) {
+      value = transformed(arg);
+    } else if (arg is Vector3) {
+      value = transformed3(arg);
+    } else if (arg is Matrix4) {
+      value = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return value;
path: lib/src/vector_math/matrix4.dart
user: login: rakudrama
body: I don't think it is worthwhile making the `dynamic` versions faster like this.
The user has to do a dynamic call or cast to use the result.
I'd rather they were deprecated with proper versioning.
created_at: 2025-04-30T16:39:41Z
author_association: COLLABORATOR
diff_hunk: @@ -638,20 +638,29 @@ class Matrix4 {
   Matrix4 operator -() => clone()..negate();
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  ///
+  /// [arg] should be a [double] (to scale), [Vector4] (to transform), [Vector3]
+  /// (to transform), or [Matrix4] (to multiply).
+  ///
+  /// If you know the argument type in a call site, prefer [scaled],
+  /// [transformed], [transformed3], or [multiplied] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final dynamic value;
path: lib/src/vector_math/matrix4.dart
user: login: osa1
body: Done.
created_at: 2025-05-01T09:11:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -321,6 +321,148 @@ class Matrix3TransposeMultiplyBenchmark extends BenchmarkBase {
   }
 }
 
+class Matrix4TranslateByDoubleGenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByDoubleGenericBenchmark()
+      : super('Matrix4.translateByDoubleGeneric');
+
+  final temp = Matrix4.zero()..setIdentity();
+
+  static void main() {
+    Matrix4TranslateByDoubleGenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(10.0, 20.0, 30.0);
+    }
+  }
+}
+
+class Matrix4TranslateByVector3GenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByVector3GenericBenchmark()
+      : super('Matrix4.translateByVector3Generic');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector3(10.0, 20.0, 30.0);
+
+  static void main() {
+    Matrix4TranslateByVector3GenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByVector4GenericBenchmark extends BenchmarkBase {
+  Matrix4TranslateByVector4GenericBenchmark()
+      : super('Matrix4.translateByVector4Generic');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector4(10.0, 20.0, 30.0, 40.0);
+
+  static void main() {
+    Matrix4TranslateByVector4GenericBenchmark().report();
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translate(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByDoubleBenchmark extends BenchmarkBase {
+  Matrix4TranslateByDoubleBenchmark() : super('Matrix4.translateByDouble');
+
+  final temp = Matrix4.zero()..setIdentity();
+
+  static void main() {
+    Matrix4TranslateByDoubleBenchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByDouble(
+          i.toDouble(), (i * 10).toDouble(), (i * 5).toDouble(), 1.0);
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByDouble(10.0, 20.0, 30.0, 1.0);
+    }
+  }
+}
+
+class Matrix4TranslateByVector3Benchmark extends BenchmarkBase {
+  Matrix4TranslateByVector3Benchmark() : super('Matrix4.translateByVector3');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector3(10.0, 20.0, 30.0);
+
+  static void main() {
+    Matrix4TranslateByVector3Benchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByVector3(
+          Vector3(i.toDouble(), (i * 10).toDouble(), (i * 5).toDouble()));
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByVector3(vec);
+    }
+  }
+}
+
+class Matrix4TranslateByVector4Benchmark extends BenchmarkBase {
+  Matrix4TranslateByVector4Benchmark() : super('Matrix4.translateByVector4');
+
+  final temp = Matrix4.zero()..setIdentity();
+  final vec = Vector4(10.0, 20.0, 30.0, 40.0);
+
+  static void main() {
+    Matrix4TranslateByVector4Benchmark().report();
+  }
+
+  // Call the benchmarked method with random arguments to make sure TFA won't
+  // specialize it based on the arguments passed and wasm-opt won't inline it,
+  // for fair comparison with the generic case.
+  @override
+  void setup() {
+    for (var i = 0; i < 10; i++) {
+      temp.translateByVector4(Vector4(i.toDouble(), (i * 10).toDouble(),
+          (i * 5).toDouble(), (i * 20).toDouble()));
+    }
+  }
+
+  @override
+  void run() {
+    for (var i = 0; i < 100; i++) {
+      temp.translateByVector4(vec);
+    }
+  }
+}
+
 void main() {
   MatrixMultiplyBenchmark.main();
path: benchmark/matrix_bench.dart
user: login: osa1
body: Let's do it separately. It's going to be a big change to update all of the benchmarks, and it can be done separately.
created_at: 2025-05-01T09:19:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -638,20 +638,29 @@ class Matrix4 {
   Matrix4 operator -() => clone()..negate();
 
   /// Returns a new vector or matrix by multiplying this with [arg].
+  ///
+  /// [arg] should be a [double] (to scale), [Vector4] (to transform), [Vector3]
+  /// (to transform), or [Matrix4] (to multiply).
+  ///
+  /// If you know the argument type in a call site, prefer [scaled],
+  /// [transformed], [transformed3], or [multiplied] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
   dynamic operator *(dynamic arg) {
+    final dynamic value;
     if (arg is double) {
-      return scaled(arg);
-    }
-    if (arg is Vector4) {
-      return transformed(arg);
-    }
-    if (arg is Vector3) {
-      return transformed3(arg);
-    }
-    if (arg is Matrix4) {
-      return multiplied(arg);
+      value = scaled(arg);
+    } else if (arg is Vector4) {
+      value = transformed(arg);
+    } else if (arg is Vector3) {
+      value = transformed3(arg);
+    } else if (arg is Matrix4) {
+      value = multiplied(arg);
+    } else {
+      throw ArgumentError(arg);
     }
-    throw ArgumentError(arg);
+    return value;
path: lib/src/vector_math/matrix4.dart
user: login: osa1
body: But it's free performance, why not make it faster?

(It's free because I assume all calls sites will directly call it, so it'll always be inlined, and the type tests will always disappear as the argument types will also known at the call sites. So no binary size increase.)

I added deprecations.
created_at: 2025-05-01T10:04:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -660,97 +669,144 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use translateByVector3, translateByVector4, '
+      'or translateByDouble instead')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      translateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z, w.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByDouble(double tx, double ty, double tz, double tw) {
     final t1 = _m4storage[0] * tx +
         _m4storage[4] * ty +
         _m4storage[8] * tz +
         _m4storage[12] * tw;
+    _m4storage[12] = t1;
+
     final t2 = _m4storage[1] * tx +
         _m4storage[5] * ty +
         _m4storage[9] * tz +
         _m4storage[13] * tw;
+    _m4storage[13] = t2;
+
     final t3 = _m4storage[2] * tx +
         _m4storage[6] * ty +
         _m4storage[10] * tz +
         _m4storage[14] * tw;
+    _m4storage[14] = t3;
+
     final t4 = _m4storage[3] * tx +
         _m4storage[7] * ty +
         _m4storage[11] * tz +
         _m4storage[15] * tw;
-    _m4storage[12] = t1;
-    _m4storage[13] = t2;
-    _m4storage[14] = t3;
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector3(Vector3 v3) =>
+      translateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Translate this matrix by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector4(Vector4 v4) =>
+      translateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Multiply this by a translation from the left.
-  /// The translation can be specified with a  [Vector3], [Vector4], or x, y, z.
+  ///
+  /// The translation can be specified with a [Vector3], [Vector4], or x, y, z
+  /// as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer
+  /// [leftTranslateByDouble], [leftTranslateByVector3], or
+  /// [leftTranslateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use leftTranslateByVector3, leftTranslateByVector4, '
+      'or leftTranslateByDouble instead')
   void leftTranslate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      leftTranslateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
path: lib/src/vector_math/matrix4.dart
user: login: kevmoo
body: I get inlining the redirecting functions. But these are BIG functions. Should we really be inlining them?
created_at: 2025-05-13T22:52:49Z
author_association: COLLABORATOR
diff_hunk: @@ -660,97 +669,144 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use translateByVector3, translateByVector4, '
+      'or translateByDouble instead')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      translateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z, w.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByDouble(double tx, double ty, double tz, double tw) {
     final t1 = _m4storage[0] * tx +
         _m4storage[4] * ty +
         _m4storage[8] * tz +
         _m4storage[12] * tw;
+    _m4storage[12] = t1;
+
     final t2 = _m4storage[1] * tx +
         _m4storage[5] * ty +
         _m4storage[9] * tz +
         _m4storage[13] * tw;
+    _m4storage[13] = t2;
+
     final t3 = _m4storage[2] * tx +
         _m4storage[6] * ty +
         _m4storage[10] * tz +
         _m4storage[14] * tw;
+    _m4storage[14] = t3;
+
     final t4 = _m4storage[3] * tx +
         _m4storage[7] * ty +
         _m4storage[11] * tz +
         _m4storage[15] * tw;
-    _m4storage[12] = t1;
-    _m4storage[13] = t2;
-    _m4storage[14] = t3;
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector3(Vector3 v3) =>
+      translateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Translate this matrix by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector4(Vector4 v4) =>
+      translateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Multiply this by a translation from the left.
-  /// The translation can be specified with a  [Vector3], [Vector4], or x, y, z.
+  ///
+  /// The translation can be specified with a [Vector3], [Vector4], or x, y, z
+  /// as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer
+  /// [leftTranslateByDouble], [leftTranslateByVector3], or
+  /// [leftTranslateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use leftTranslateByVector3, leftTranslateByVector4, '
+      'or leftTranslateByDouble instead')
   void leftTranslate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      leftTranslateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
path: lib/src/vector_math/matrix4.dart
user: login: kevmoo
body: @osa1 – wondering if we should hold off on the 2nd level of inlining until we know it's a net win...maybe?

CC @rakudrama 
created_at: 2025-05-13T22:58:56Z
author_association: COLLABORATOR
diff_hunk: @@ -660,97 +669,144 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use translateByVector3, translateByVector4, '
+      'or translateByDouble instead')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      translateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z, w.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByDouble(double tx, double ty, double tz, double tw) {
     final t1 = _m4storage[0] * tx +
         _m4storage[4] * ty +
         _m4storage[8] * tz +
         _m4storage[12] * tw;
+    _m4storage[12] = t1;
+
     final t2 = _m4storage[1] * tx +
         _m4storage[5] * ty +
         _m4storage[9] * tz +
         _m4storage[13] * tw;
+    _m4storage[13] = t2;
+
     final t3 = _m4storage[2] * tx +
         _m4storage[6] * ty +
         _m4storage[10] * tz +
         _m4storage[14] * tw;
+    _m4storage[14] = t3;
+
     final t4 = _m4storage[3] * tx +
         _m4storage[7] * ty +
         _m4storage[11] * tz +
         _m4storage[15] * tw;
-    _m4storage[12] = t1;
-    _m4storage[13] = t2;
-    _m4storage[14] = t3;
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector3(Vector3 v3) =>
+      translateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Translate this matrix by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector4(Vector4 v4) =>
+      translateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Multiply this by a translation from the left.
-  /// The translation can be specified with a  [Vector3], [Vector4], or x, y, z.
+  ///
+  /// The translation can be specified with a [Vector3], [Vector4], or x, y, z
+  /// as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer
+  /// [leftTranslateByDouble], [leftTranslateByVector3], or
+  /// [leftTranslateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use leftTranslateByVector3, leftTranslateByVector4, '
+      'or leftTranslateByDouble instead')
   void leftTranslate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      leftTranslateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
path: lib/src/vector_math/matrix4.dart
user: login: rakudrama
body: This could be counterproductive for JavaScript. I'd rather we didn't prefer-inline for dart2js.

JavaScript has its own JIT, and that can do inlining itself.
If this function is not inlined by dart2js, and super hot, it will be JIT-optimized.
Then all the call sites benefit, even if most of them are not hot.

Over-inlining can cause the host function less likely to be JIT-ed, even if it is hot.
Either is is simply too big to to attempt JIT optimization, or there are more opportunities for de-optimization, and eventually the JIT-optimizer declines to make further attempts.

Any function in JavaScript with more than a few operations is at risk of these counter-productive behaviors.

created_at: 2025-05-13T23:14:48Z
author_association: COLLABORATOR
diff_hunk: @@ -660,97 +669,144 @@ class Matrix4 {
   /// Returns new matrix after component wise this - [arg]
   Matrix4 operator -(Matrix4 arg) => clone()..sub(arg);
 
-  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z
+  /// Translate this matrix by a [Vector3], [Vector4], or x,y,z as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer [translateByDouble],
+  /// [translateByVector3], or [translateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use translateByVector3, translateByVector4, '
+      'or translateByDouble instead')
   void translate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      translateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      translateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
+
+  /// Translate this matrix by x, y, z, w.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByDouble(double tx, double ty, double tz, double tw) {
     final t1 = _m4storage[0] * tx +
         _m4storage[4] * ty +
         _m4storage[8] * tz +
         _m4storage[12] * tw;
+    _m4storage[12] = t1;
+
     final t2 = _m4storage[1] * tx +
         _m4storage[5] * ty +
         _m4storage[9] * tz +
         _m4storage[13] * tw;
+    _m4storage[13] = t2;
+
     final t3 = _m4storage[2] * tx +
         _m4storage[6] * ty +
         _m4storage[10] * tz +
         _m4storage[14] * tw;
+    _m4storage[14] = t3;
+
     final t4 = _m4storage[3] * tx +
         _m4storage[7] * ty +
         _m4storage[11] * tz +
         _m4storage[15] * tw;
-    _m4storage[12] = t1;
-    _m4storage[13] = t2;
-    _m4storage[14] = t3;
     _m4storage[15] = t4;
   }
 
+  /// Translate this matrix by a [Vector3].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector3(Vector3 v3) =>
+      translateByDouble(v3.x, v3.y, v3.z, 1.0);
+
+  /// Translate this matrix by a [Vector4].
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  void translateByVector4(Vector4 v4) =>
+      translateByDouble(v4.x, v4.y, v4.z, v4.w);
+
   /// Multiply this by a translation from the left.
-  /// The translation can be specified with a  [Vector3], [Vector4], or x, y, z.
+  ///
+  /// The translation can be specified with a [Vector3], [Vector4], or x, y, z
+  /// as [double]s.
+  ///
+  /// If you know the argument types in a call site, prefer
+  /// [leftTranslateByDouble], [leftTranslateByVector3], or
+  /// [leftTranslateByVector4] for performance.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
+  @Deprecated('Use leftTranslateByVector3, leftTranslateByVector4, '
+      'or leftTranslateByDouble instead')
   void leftTranslate(dynamic x, [double y = 0.0, double z = 0.0]) {
-    double tx;
-    double ty;
-    double tz;
-    final tw = x is Vector4 ? x.w : 1.0;
     if (x is Vector3) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector3(x);
     } else if (x is Vector4) {
-      tx = x.x;
-      ty = x.y;
-      tz = x.z;
+      leftTranslateByVector4(x);
     } else if (x is double) {
-      tx = x;
-      ty = y;
-      tz = z;
+      leftTranslateByDouble(x, y, z, 1.0);
     } else {
       throw UnimplementedError();
     }
+  }
 
+  /// Multiply this by a translation from the left.
+  @pragma('wasm:prefer-inline')
+  @pragma('vm:prefer-inline')
+  @pragma('dart2js:prefer-inline')
path: lib/src/vector_math/matrix4.dart
user: login: kevmoo
body: See https://github.com/google/vector_math.dart/pull/347/
created_at: 2025-05-14T00:48:59Z
author_association: COLLABORATOR

PR comments:
user: login: coveralls
created_at: 2025-04-16T11:38:36Z
body: 
[![Coverage Status](https://coveralls.io/builds/73469306/badge)](https://coveralls.io/builds/73469306)

coverage: 26.388% (+0.02%) from 26.372%
when pulling **7fb9a0247966f33b30f5f50af8137d4085d9a6e6 on osa1:faster_vector4_translate**
into **39cafd45346f355a6e4c38a17c89fcd199dc1c96 on google:master**.

author_association: NONE
user: login: osa1
created_at: 2025-04-16T11:46:48Z
body: Somehow I can't add reviewers to the PR.

@mkustermann @eyebrowsoffire could you have a look?
author_association: CONTRIBUTOR
user: login: spydon
created_at: 2025-04-16T11:55:16Z
body: Maybe it's time to make a breaking release soon instead? Then I would remove all the dynamic arguments from all the classes, and we would also follow semver for #270
author_association: CONTRIBUTOR
user: login: osa1
created_at: 2025-04-16T13:16:28Z
body: This change doesn't need to be a breaking change. We can add a deprecation to `translate` though as I would think that it's too slow for what it's intended for.
author_association: CONTRIBUTOR
user: login: spydon
created_at: 2025-04-16T13:37:31Z
body: > This change doesn't need to be a breaking change. We can add a deprecation to `translate` though as I would think that it's too slow for what it's intended for.

Yeah, it wouldn't be a breaking change right now. But if following semver, marking it as deprecated is just the first step towards a breaking change, when removing the deprecated method major should be bumped (https://semver.org/#how-should-i-handle-deprecating-functionality).

I'm not too fond of all the `byType` methods, but I also can't come up with a better solution, and it is definitely better than taking `dynamic` as an argument.
author_association: CONTRIBUTOR
user: login: kevmoo
created_at: 2025-04-25T17:11:54Z
body: Let's get a changelog in here and bump the minor (_.X._) version
author_association: COLLABORATOR
user: login: kevmoo
created_at: 2025-04-25T17:48:21Z
body: @osa1 – also, would you add the base value to the benchmarks? I don't see how much better these are than the base versions
author_association: COLLABORATOR
user: login: kevmoo
created_at: 2025-04-25T18:08:25Z
body: @osa1 – by all means revert if you don't like the last commit
author_association: COLLABORATOR
user: login: osa1
created_at: 2025-04-29T11:17:50Z
body: > by all means revert if you don't like the last commit

I dropped your commit as it changed benchmark results and made them worse, and then I had to debug this because I wasn't able to reproduce the benchmark results in my PR description. 

(The "cleanup" change calls less efficient function from more efficient one and makes `translateByVector3` benchmark worse)
author_association: CONTRIBUTOR
user: login: osa1
created_at: 2025-04-30T10:55:44Z
body: @mraleph @rakudrama @mkustermann I think I addressed all of the feedback. Could you PTAL? Start with the PR description, I just applied the same changes to other functions that take `dynamic` arguments.

@kevmoo as far as I know still no one from the backend teams can approve reviews, can we give at least one person from each backend review rights? They already reviewed this PR, they just officially can't approve.

(Same as SDK, it makes sense for changes to libraries to be reviewed by backend teams to catch any performance or binary size issues)
author_association: CONTRIBUTOR
user: login: osa1
created_at: 2025-04-30T12:26:59Z
body: > move stores up as far as possible

Done. I haven't benchmarked this change in isolation.

> have single implementation that all others delegate to

Done. Doesn't affect performance when I inline everything.

> make functions only have a single return & mark as @pragma('dart2js:prefer-inline')

Done. Improves dart2js performance.

> access [15] first

NOT done: doesn't make any difference in dart2wasm and AOT, but makes dart2js perform worse.

---

~~I'll update benchmark results in the PR description in a bit.~~ Done.
author_association: CONTRIBUTOR
user: login: rakudrama
created_at: 2025-04-30T16:26:22Z
body: > Generally LGTM with the following comments
> 
> * move stores up as far as possible

I'm worried about moving stores. There are constructors that take 'storage' as an input, so there can be aliasing via typed data views.
If you move a store, there is an obligation to explain to the reader why the order is safe for aliased inputs.
Yes, there are lots of bugs already with aliasing, but lets not add more.

> * access `[15]` first

Can we make the statement check `_m4storage[15];` work on all platforms? It works for VM-AOT and dart2js.
Moving statements for the effect of moving `[15]` first is less readable and open to changing read-write order that may affect aliased inputs.

> * have single implementation that all others delegate to
> * make functions only have a single return & mark as `@pragma('dart2js:prefer-inline')`
> 
> If the last point improves performance in dart2js similar to dart2wasm & vm, then do we get meaningful perf benefits from having the `{translate,scale,...}By{Double,...}` operations?


author_association: COLLABORATOR
user: login: osa1
created_at: 2025-05-01T09:18:32Z
body: > I'm worried about moving stores. There are constructors that take 'storage' as an input, so there can be aliasing via typed data views.
> If you move a store, there is an obligation to explain to the reader why the order is safe for aliased inputs.
> Yes, there are lots of bugs already with aliasing, but lets not add more.

Which function(s) do you mean that can have aliasing bugs?

The code with stores moved up is this:

```dart
void translateByDouble(double tx, double ty, double tz, double tw) {
  final t1 = _m4storage[0] * tx +
      _m4storage[4] * ty +
      _m4storage[8] * tz +
      _m4storage[12] * tw;
  _m4storage[12] = t1;

  final t2 = _m4storage[1] * tx +
      _m4storage[5] * ty +
      _m4storage[9] * tz +
      _m4storage[13] * tw;
  _m4storage[13] = t2;

  final t3 = _m4storage[2] * tx +
      _m4storage[6] * ty +
      _m4storage[10] * tz +
      _m4storage[14] * tw;
  _m4storage[14] = t3;

  final t4 = _m4storage[3] * tx +
      _m4storage[7] * ty +
      _m4storage[11] * tz +
      _m4storage[15] * tw;
  _m4storage[15] = t4;
}
```

There's only one array being used here, so aliasing can't be an issue.

> Can we make the statement check _m4storage[15]; work on all platforms? 

I reported benchmarks with this here: https://github.com/google/vector_math.dart/pull/345#discussion_r2063389469.

It doesn't make any difference in any of: AOT, dart2js, dart2wasm.

It's difficult to make it work with dart2wasm because wasm-opt doesn't track accessed locations and omit bounds checks based on that. As mentioned in the thread, I reported this use case to the wasm-opt team and but it's in the backlog with low priority.
author_association: CONTRIBUTOR
user: login: osa1
created_at: 2025-05-01T10:02:25Z
body: I think all feedback is addressed now. @kevmoo we can merge this.

Make sure to squash and copy/paste PR description as the commit message. GitHub's UI messes with formatting when it automatically inserts PR description as the commit message when you click on "squash and merge".
author_association: CONTRIBUTOR
user: login: osa1
created_at: 2025-05-01T10:09:57Z
body: Bumped major version instead as this is a breaking change. (new membrers added to non-final class)
author_association: CONTRIBUTOR
user: login: osa1
created_at: 2025-05-01T10:20:36Z
body: Per Martin's feedback I reverted the major version bump.

Flutter uses this package, and when it migrates to the new major version other packages that are commonly used with Flutter that also use vector_math won't compile. To avoid this we bump minor version.

The change is technically breaking (new members added to non-final class), but you shouldn't extend these classes anyway (kills performance). So hopefully no one's done it.
author_association: CONTRIBUTOR
user: login: kevmoo
created_at: 2025-05-12T21:56:15Z
body: https://github.com/dart-lang/sdk/commit/3b444c590a72a675ba5e9660f0c1b0fa512e86d6 landed and I'm guessing this rolled to Google3. 

Should we do a release and get this rolled to Flutter?

Should we do a verification pass on Flutter first?
author_association: COLLABORATOR
user: login: osa1
created_at: 2025-05-13T07:55:58Z
body: I think ideally for a package this small and simple we shouldn't need rolls for testing, we should be able to test it properly with just `dart test` and release.

I don't know how comprehensive testing in this package is as I did just one commit to the package, but I see tests for all the changed functions: `translate`, `leftTranslate`, `scale`, `multiply`.

Given that the changes are simple, and we have unit tests covering each of the changed functions, I would just publish.
author_association: CONTRIBUTOR