From f710c4338a82b73e780519893cba959b38db1778 Mon Sep 17 00:00:00 2001 From: Hakim Jonas Ghoula Date: Fri, 29 May 2026 00:03:32 -0700 Subject: [PATCH] [dart2wasm] Intrinsify math.min() and math.max() for double and int MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #55173. Exposes signed min/max on WasmI64 and f64.min/f64.max on WasmF64 in dart:_wasm, mirroring how WasmF64.sqrt is exposed today. The dart:math min/max patches in math_patch.dart dispatch to them via runtime `is`-checks, with @pragma('wasm:prefer-inline') so the inliner folds the chain to the bare instruction sequence at each call site: T min(T a, T b) { if (a is int && b is int) return unsafeCast((a as int).minS(b)); if (a is double && b is double) return unsafeCast((a as double).min(b)); return _minSlow(a, b); } Wasm has no native i64 min_s/max_s, so WasmI64.minS/maxS emit the same local.tee + i64.le_s/ge_s + select sequence. WasmF64.min/max emit f64.min/f64.max directly. The NaN- and signed-zero-aware fallback ladder is preserved in `_minSlow` / `_maxSlow` (out-of-line, no pragma) and called for the mixed and num cases. tests/lib/math/min_max_test.dart requires type preservation between equal int and double arguments (e.g. min(-499, -499.0) is int at line 113; max(499, 499.0) is int at line 382), which a toDouble().max(toDouble()) fallback would not satisfy. Adds pkg/dart2wasm/test/ir_tests/math_min_max.dart covering min/max for static int/int, double/double, mixed int/double, and num/num. The .wat locks in f64.min/f64.max for the f64 paths, i64.le_s/i64.ge_s + select for the i64 paths, and `call $_minSlow` / `call $_maxSlow` for mixed and num/num. Measurements on a probe with four typed call sites (one each for min, max, min, max, all marked @pragma('wasm:never-inline')): * .wasm size: 27,112 → 25,848 bytes (-4.66%). Generic $min and $max are eliminated by DCE. * Runtime, 100M iterations per operation on d8, median of 10 runs: min 543 → 213 ms (2.55x), max 550 → 213 ms (2.58x), min 552 → 65 ms (8.49x), max 555 → 73 ms (7.61x). Checksums match between baseline and patched. tests/lib/math/min_max_test.dart passes. R=mkustermann@google.com, osa1@google.com Change-Id: If8cf0a4df976f2d7f2230308905ff68491311c97 Bug: https://github.com/dart-lang/sdk/issues/55173 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/503740 Reviewed-by: Martin Kustermann Reviewed-by: Kevin Moore Reviewed-by: Slava Egorov Commit-Queue: Martin Kustermann --- pkg/dart2wasm/lib/intrinsics.dart | 36 +++ pkg/dart2wasm/test/ir_tests/math_min_max.dart | 64 +++++ pkg/dart2wasm/test/ir_tests/math_min_max.wat | 219 ++++++++++++++++++ sdk/lib/_internal/wasm/common/math_patch.dart | 45 ++-- sdk/lib/_wasm/wasm_types.dart | 30 +++ 5 files changed, 366 insertions(+), 28 deletions(-) create mode 100644 pkg/dart2wasm/test/ir_tests/math_min_max.dart create mode 100644 pkg/dart2wasm/test/ir_tests/math_min_max.wat diff --git a/pkg/dart2wasm/lib/intrinsics.dart b/pkg/dart2wasm/lib/intrinsics.dart index 62b64dc6e05..47424bcd982 100644 --- a/pkg/dart2wasm/lib/intrinsics.dart +++ b/pkg/dart2wasm/lib/intrinsics.dart @@ -821,6 +821,26 @@ class Intrinsifier { ); b.i64_div_s(); return w.NumType.i64; + case "minS": + case "maxS": + w.Local localA = b.addLocal(w.NumType.i64); + w.Local localB = b.addLocal(w.NumType.i64); + codeGen.translateExpression(receiver, w.NumType.i64); + b.local_tee(localA); + codeGen.translateExpression( + node.arguments.positional[0], + w.NumType.i64, + ); + b.local_tee(localB); + b.local_get(localA); + b.local_get(localB); + if (name == "minS") { + b.i64_le_s(); + } else { + b.i64_ge_s(); + } + b.select(w.NumType.i64); + return w.NumType.i64; default: throw 'Unknown WasmI64 member $name'; } @@ -854,6 +874,22 @@ class Intrinsifier { ); b.f64_copysign(); return w.NumType.f64; + case "min": + codeGen.translateExpression(receiver, w.NumType.f64); + codeGen.translateExpression( + node.arguments.positional[0], + w.NumType.f64, + ); + b.f64_min(); + return w.NumType.f64; + case "max": + codeGen.translateExpression(receiver, w.NumType.f64); + codeGen.translateExpression( + node.arguments.positional[0], + w.NumType.f64, + ); + b.f64_max(); + return w.NumType.f64; default: throw 'Unknown WasmF64 member $name'; } diff --git a/pkg/dart2wasm/test/ir_tests/math_min_max.dart b/pkg/dart2wasm/test/ir_tests/math_min_max.dart new file mode 100644 index 00000000000..314806eed04 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/math_min_max.dart @@ -0,0 +1,64 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// functionFilter=test|sink +// globalFilter=DoesNotMatch +// tableFilter=DoesNotMatch +// compilerOption=-O2 + +import 'dart:math' as math; + +final ktrue = int.parse('1') == 1; + +final intA = ktrue ? 1 : 2; +final intB = ktrue ? 2 : 1; +final doubleA = ktrue ? 1.5 : 2.5; +final doubleB = ktrue ? 2.5 : 1.5; +final numIntA = ktrue ? intA : 1.2; +final numIntB = ktrue ? intB : 1.2; + +@pragma('wasm:never-inline') +void main() { + testMinIntInt(); + testMaxIntInt(); + testMinDoubleDouble(); + testMaxDoubleDouble(); + testMinIntDouble(); + testMaxIntDouble(); + testMinNumNum(); + testMaxNumNum(); +} + +@pragma('wasm:never-inline') +void testMinIntInt() => sinkInt(math.min(intA, intB)); + +@pragma('wasm:never-inline') +void testMaxIntInt() => sinkInt(math.max(intA, intB)); + +@pragma('wasm:never-inline') +void testMinDoubleDouble() => sinkDouble(math.min(doubleA, doubleB)); + +@pragma('wasm:never-inline') +void testMaxDoubleDouble() => sinkDouble(math.max(doubleA, doubleB)); + +@pragma('wasm:never-inline') +void testMinIntDouble() => sinkNum(math.min(intA, doubleA)); + +@pragma('wasm:never-inline') +void testMaxIntDouble() => sinkNum(math.max(intA, doubleA)); + +@pragma('wasm:never-inline') +void testMinNumNum() => sinkNum(math.min(numIntA, numIntB)); + +@pragma('wasm:never-inline') +void testMaxNumNum() => sinkNum(math.max(numIntA, numIntB)); + +@pragma('wasm:never-inline') +void sinkInt(int x) => print(x); + +@pragma('wasm:never-inline') +void sinkDouble(double x) => print(x); + +@pragma('wasm:never-inline') +void sinkNum(num x) => print(x); diff --git a/pkg/dart2wasm/test/ir_tests/math_min_max.wat b/pkg/dart2wasm/test/ir_tests/math_min_max.wat new file mode 100644 index 00000000000..e8f26e32da0 --- /dev/null +++ b/pkg/dart2wasm/test/ir_tests/math_min_max.wat @@ -0,0 +1,219 @@ +(module $$ + (type $#Top (struct + (field $field0 i32))) + (type $BoxedDouble (sub final $#Top (struct + (field $field0 i32) + (field $value f64)))) + (type $BoxedInt (sub final $#Top (struct + (field $field0 i32) + (field $value i64)))) + (func $doubleA implicit getter (result f64) <...>) + (func $doubleB implicit getter (result f64) <...>) + (func $intA implicit getter (result i64) <...>) + (func $intB implicit getter (result i64) <...>) + (func $numIntA implicit getter (result (ref $#Top)) <...>) + (func $numIntB implicit getter (result (ref $#Top)) <...>) + (func $"sinkDouble " (param $var0 f64) + i32.const 92 + local.get $var0 + struct.new $BoxedDouble + call $print + ) + (func $"sinkInt " (param $var0 i64) + i32.const 86 + local.get $var0 + struct.new $BoxedInt + call $print + ) + (func $"sinkNum " (param $var0 (ref $#Top)) + local.get $var0 + call $print + ) + (func $"testMaxDoubleDouble " + call $"doubleA implicit getter" + call $"doubleB implicit getter" + f64.max + call $"sinkDouble " + ) + (func $"testMaxIntDouble " + i32.const 86 + call $"intA implicit getter" + struct.new $BoxedInt + i32.const 92 + call $"doubleA implicit getter" + struct.new $BoxedDouble + call $_maxSlow + call $"sinkNum " + ) + (func $"testMaxIntInt " + (local $var0 i64) + (local $var1 i64) + call $"intA implicit getter" + local.tee $var0 + call $"intB implicit getter" + local.tee $var1 + local.get $var0 + local.get $var1 + i64.ge_s + select + call $"sinkInt " + ) + (func $"testMaxNumNum " + (local $var0 (ref $#Top)) + (local $var1 (ref $#Top)) + (local $var2 i64) + (local $var3 i64) + block $label0 (result (ref $#Top)) + call $"numIntA implicit getter" + local.tee $var0 + struct.get $#Top $field0 + i32.const 86 + i32.ne + call $"numIntB implicit getter" + local.tee $var1 + struct.get $#Top $field0 + i32.const 86 + i32.ne + i32.or + i32.eqz + if + i32.const 86 + local.get $var0 + ref.cast $BoxedInt + struct.get $BoxedInt $value + local.tee $var2 + local.get $var1 + ref.cast $BoxedInt + struct.get $BoxedInt $value + local.tee $var3 + local.get $var2 + local.get $var3 + i64.ge_s + select + struct.new $BoxedInt + br $label0 + end + local.get $var0 + struct.get $#Top $field0 + i32.const 92 + i32.ne + local.get $var1 + struct.get $#Top $field0 + i32.const 92 + i32.ne + i32.or + i32.eqz + if + i32.const 92 + local.get $var0 + ref.cast $BoxedDouble + struct.get $BoxedDouble $value + local.get $var1 + ref.cast $BoxedDouble + struct.get $BoxedDouble $value + f64.max + struct.new $BoxedDouble + br $label0 + end + local.get $var0 + local.get $var1 + call $_maxSlow + end $label0 + call $"sinkNum " + ) + (func $"testMinDoubleDouble " + call $"doubleA implicit getter" + call $"doubleB implicit getter" + f64.min + call $"sinkDouble " + ) + (func $"testMinIntDouble " + i32.const 86 + call $"intA implicit getter" + struct.new $BoxedInt + i32.const 92 + call $"doubleA implicit getter" + struct.new $BoxedDouble + call $_minSlow + call $"sinkNum " + ) + (func $"testMinIntInt " + (local $var0 i64) + (local $var1 i64) + call $"intA implicit getter" + local.tee $var0 + call $"intB implicit getter" + local.tee $var1 + local.get $var0 + local.get $var1 + i64.le_s + select + call $"sinkInt " + ) + (func $"testMinNumNum " + (local $var0 (ref $#Top)) + (local $var1 (ref $#Top)) + (local $var2 i64) + (local $var3 i64) + block $label0 (result (ref $#Top)) + call $"numIntA implicit getter" + local.tee $var0 + struct.get $#Top $field0 + i32.const 86 + i32.ne + call $"numIntB implicit getter" + local.tee $var1 + struct.get $#Top $field0 + i32.const 86 + i32.ne + i32.or + i32.eqz + if + i32.const 86 + local.get $var0 + ref.cast $BoxedInt + struct.get $BoxedInt $value + local.tee $var2 + local.get $var1 + ref.cast $BoxedInt + struct.get $BoxedInt $value + local.tee $var3 + local.get $var2 + local.get $var3 + i64.le_s + select + struct.new $BoxedInt + br $label0 + end + local.get $var0 + struct.get $#Top $field0 + i32.const 92 + i32.ne + local.get $var1 + struct.get $#Top $field0 + i32.const 92 + i32.ne + i32.or + i32.eqz + if + i32.const 92 + local.get $var0 + ref.cast $BoxedDouble + struct.get $BoxedDouble $value + local.get $var1 + ref.cast $BoxedDouble + struct.get $BoxedDouble $value + f64.min + struct.new $BoxedDouble + br $label0 + end + local.get $var0 + local.get $var1 + call $_minSlow + end $label0 + call $"sinkNum " + ) + (func $_maxSlow (param $var0 (ref $#Top)) (param $var1 (ref $#Top)) (result (ref $#Top)) <...>) + (func $_minSlow (param $var0 (ref $#Top)) (param $var1 (ref $#Top)) (result (ref $#Top)) <...>) + (func $print (param $var0 (ref $#Top)) <...>) +) \ No newline at end of file diff --git a/sdk/lib/_internal/wasm/common/math_patch.dart b/sdk/lib/_internal/wasm/common/math_patch.dart index ec19fa61bc3..27921e54bf2 100644 --- a/sdk/lib/_internal/wasm/common/math_patch.dart +++ b/sdk/lib/_internal/wasm/common/math_patch.dart @@ -3,55 +3,44 @@ // BSD-style license that can be found in the LICENSE file. import "dart:_error_utils"; -import "dart:_internal" show mix64, patch; +import "dart:_internal" show mix64, patch, unsafeCast; import "dart:_wasm"; /// There are no parts of this patch library. @patch +@pragma('wasm:prefer-inline') T min(T a, T b) { + if (a is int && b is int) return unsafeCast((a as int).minS(b)); + if (a is double && b is double) return unsafeCast((a as double).min(b)); + return _minSlow(a, b); +} + +@patch +@pragma('wasm:prefer-inline') +T max(T a, T b) { + if (a is int && b is int) return unsafeCast((a as int).maxS(b)); + if (a is double && b is double) return unsafeCast((a as double).max(b)); + return _maxSlow(a, b); +} + +T _minSlow(T a, T b) { if (a > b) return b; if (a < b) return a; if (b is double) { - // Special case for NaN and -0.0. If one argument is NaN return NaN. - // [min] must also distinguish between -0.0 and 0.0. - if (a is double) { - if (a == 0.0) { - // a is either 0.0 or -0.0. b is either 0.0, -0.0 or NaN. - // The following returns -0.0 if either a or b is -0.0, and it - // returns NaN if b is NaN. - num n = (a + b) * a * b; - return n as T; - } - } - // Check for NaN and b == -0.0. if (a == 0 && b.isNegative || b.isNaN) return b; return a; } return a; } -@patch -T max(T a, T b) { +T _maxSlow(T a, T b) { if (a > b) return a; if (a < b) return b; if (b is double) { - // Special case for NaN and -0.0. If one argument is NaN return NaN. - // [max] must also distinguish between -0.0 and 0.0. - if (a is double) { - if (a == 0.0) { - // a is either 0.0 or -0.0. b is either 0.0, -0.0, or NaN. - // The following returns 0.0 if either a or b is 0.0, and it - // returns NaN if b is NaN. - num n = a + b; - return n as T; - } - } - // Check for NaN. if (b.isNaN) return b; return a; } - // max(-0.0, 0) must return 0. if (b == 0 && a.isNegative) return b; return a; } diff --git a/sdk/lib/_wasm/wasm_types.dart b/sdk/lib/_wasm/wasm_types.dart index 46e34438c40..490487fd330 100644 --- a/sdk/lib/_wasm/wasm_types.dart +++ b/sdk/lib/_wasm/wasm_types.dart @@ -238,6 +238,12 @@ class WasmI64 extends _WasmBase { /// Wasm `i64.div_s` instruction. external WasmI64 divS(WasmI64 divisor); + + /// Signed minimum via `i64.le_s` and `select`. + external WasmI64 minS(WasmI64 other); + + /// Signed maximum via `i64.ge_s` and `select`. + external WasmI64 maxS(WasmI64 other); } /// The Wasm `f32` type. @@ -279,6 +285,12 @@ class WasmF64 extends _WasmBase { /// Wasm `f64.copysign` instruction. external WasmF64 copysign(WasmF64 other); + + /// Wasm `f64.min` instruction. + external WasmF64 min(WasmF64 other); + + /// Wasm `f64.max` instruction. + external WasmF64 max(WasmF64 other); } /// The Wasm `v128` type. @@ -766,6 +778,14 @@ extension IntWasmInstructions on int { /// Wasm `i64.div_s` instruction. @pragma("wasm:prefer-inline") int divS(int divisor) => this.toWasmI64().divS(divisor.toWasmI64()).toInt(); + + /// Signed minimum via `i64.le_s` and `select`. + @pragma("wasm:prefer-inline") + int minS(int other) => this.toWasmI64().minS(other.toWasmI64()).toInt(); + + /// Signed maximum via `i64.ge_s` and `select`. + @pragma("wasm:prefer-inline") + int maxS(int other) => this.toWasmI64().maxS(other.toWasmI64()).toInt(); } extension DoubleWasmInstructions on double { @@ -787,6 +807,16 @@ extension DoubleWasmInstructions on double { /// Wasm `f64.sqrt` instruction. @pragma("wasm:prefer-inline") double sqrt() => this.toWasmF64().sqrt().toDouble(); + + /// Wasm `f64.min` instruction. + @pragma("wasm:prefer-inline") + double min(double other) => + this.toWasmF64().min(other.toWasmF64()).toDouble(); + + /// Wasm `f64.max` instruction. + @pragma("wasm:prefer-inline") + double max(double other) => + this.toWasmF64().max(other.toWasmF64()).toDouble(); } // Tests whether the given object's class is a subclass of T.