[dart2wasm] Intrinsify math.min() and math.max() for double and int
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 extends num>(T a, T b) { if (a is int && b is int) return unsafeCast<T>((a as int).minS(b)); if (a is double && b is double) return unsafeCast<T>((a as double).min(b)); return _minSlow<T>(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<double>, max<double>, min<int>, max<int>, 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<double> 543 → 213 ms (2.55x), max<double> 550 → 213 ms (2.58x), min<int> 552 → 65 ms (8.49x), max<int> 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 <kustermann@google.com> Reviewed-by: Kevin Moore <kevmoo@google.com> Reviewed-by: Slava Egorov <vegorov@google.com> Commit-Queue: Martin Kustermann <kustermann@google.com>
This commit is contained in:
committed by
dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
06bdbd777f
commit
f710c4338a
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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 <noInline>" (param $var0 f64)
|
||||
i32.const 92
|
||||
local.get $var0
|
||||
struct.new $BoxedDouble
|
||||
call $print
|
||||
)
|
||||
(func $"sinkInt <noInline>" (param $var0 i64)
|
||||
i32.const 86
|
||||
local.get $var0
|
||||
struct.new $BoxedInt
|
||||
call $print
|
||||
)
|
||||
(func $"sinkNum <noInline>" (param $var0 (ref $#Top))
|
||||
local.get $var0
|
||||
call $print
|
||||
)
|
||||
(func $"testMaxDoubleDouble <noInline>"
|
||||
call $"doubleA implicit getter"
|
||||
call $"doubleB implicit getter"
|
||||
f64.max
|
||||
call $"sinkDouble <noInline>"
|
||||
)
|
||||
(func $"testMaxIntDouble <noInline>"
|
||||
i32.const 86
|
||||
call $"intA implicit getter"
|
||||
struct.new $BoxedInt
|
||||
i32.const 92
|
||||
call $"doubleA implicit getter"
|
||||
struct.new $BoxedDouble
|
||||
call $_maxSlow
|
||||
call $"sinkNum <noInline>"
|
||||
)
|
||||
(func $"testMaxIntInt <noInline>"
|
||||
(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 <noInline>"
|
||||
)
|
||||
(func $"testMaxNumNum <noInline>"
|
||||
(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 <noInline>"
|
||||
)
|
||||
(func $"testMinDoubleDouble <noInline>"
|
||||
call $"doubleA implicit getter"
|
||||
call $"doubleB implicit getter"
|
||||
f64.min
|
||||
call $"sinkDouble <noInline>"
|
||||
)
|
||||
(func $"testMinIntDouble <noInline>"
|
||||
i32.const 86
|
||||
call $"intA implicit getter"
|
||||
struct.new $BoxedInt
|
||||
i32.const 92
|
||||
call $"doubleA implicit getter"
|
||||
struct.new $BoxedDouble
|
||||
call $_minSlow
|
||||
call $"sinkNum <noInline>"
|
||||
)
|
||||
(func $"testMinIntInt <noInline>"
|
||||
(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 <noInline>"
|
||||
)
|
||||
(func $"testMinNumNum <noInline>"
|
||||
(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 <noInline>"
|
||||
)
|
||||
(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)) <...>)
|
||||
)
|
||||
@@ -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 extends num>(T a, T b) {
|
||||
if (a is int && b is int) return unsafeCast<T>((a as int).minS(b));
|
||||
if (a is double && b is double) return unsafeCast<T>((a as double).min(b));
|
||||
return _minSlow<T>(a, b);
|
||||
}
|
||||
|
||||
@patch
|
||||
@pragma('wasm:prefer-inline')
|
||||
T max<T extends num>(T a, T b) {
|
||||
if (a is int && b is int) return unsafeCast<T>((a as int).maxS(b));
|
||||
if (a is double && b is double) return unsafeCast<T>((a as double).max(b));
|
||||
return _maxSlow<T>(a, b);
|
||||
}
|
||||
|
||||
T _minSlow<T extends num>(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 extends num>(T a, T b) {
|
||||
T _maxSlow<T extends num>(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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user