diff --git a/benchmarks/Omnibus/dart/Omnibus.dart b/benchmarks/Omnibus/dart/Omnibus.dart index 07a8da4f3a0..393f4c93ddc 100644 --- a/benchmarks/Omnibus/dart/Omnibus.dart +++ b/benchmarks/Omnibus/dart/Omnibus.dart @@ -25,6 +25,7 @@ import '../../SHA1/dart/sha1.dart' as lib_SHA1; import '../../SHA256/dart/sha256.dart' as lib_SHA256; import '../../SkeletalAnimation/dart/SkeletalAnimation.dart' as lib_SkeletalAnimation; +import '../../SimdInt32x4/dart/SimdInt32x4.dart' as lib_SimdInt32x4; import '../../SkeletalAnimationSIMD/dart/SkeletalAnimationSIMD.dart' as lib_SkeletalAnimationSIMD; import '../../SwitchFSM/dart/SwitchFSM.dart' as lib_SwitchFSM; @@ -45,6 +46,7 @@ final Map benchmarks = { 'RuntimeType': lib_RuntimeType.main, 'SHA1': lib_SHA1.main, 'SHA256': lib_SHA256.main, + 'SimdInt32x4': lib_SimdInt32x4.main, 'SkeletalAnimation': lib_SkeletalAnimation.main, 'SkeletalAnimationSIMD': lib_SkeletalAnimationSIMD.main, 'SwitchFSM': lib_SwitchFSM.main, diff --git a/benchmarks/OmnibusDeferred/dart/OmnibusDeferred.dart b/benchmarks/OmnibusDeferred/dart/OmnibusDeferred.dart index c540abab8c3..206decf11ac 100644 --- a/benchmarks/OmnibusDeferred/dart/OmnibusDeferred.dart +++ b/benchmarks/OmnibusDeferred/dart/OmnibusDeferred.dart @@ -24,6 +24,7 @@ import '../../RecordCollections/dart/RecordCollections.dart' import '../../RuntimeType/dart/RuntimeType.dart' deferred as lib_RuntimeType; import '../../SHA1/dart/sha1.dart' deferred as lib_SHA1; import '../../SHA256/dart/sha256.dart' deferred as lib_SHA256; +import '../../SimdInt32x4/dart/SimdInt32x4.dart' deferred as lib_SimdInt32x4; import '../../SkeletalAnimation/dart/SkeletalAnimation.dart' deferred as lib_SkeletalAnimation; import '../../SkeletalAnimationSIMD/dart/SkeletalAnimationSIMD.dart' @@ -61,6 +62,7 @@ final Map benchmarks = { 'RuntimeType': Lib(lib_RuntimeType.loadLibrary, () => lib_RuntimeType.main()), 'SHA1': Lib(lib_SHA1.loadLibrary, () => lib_SHA1.main()), 'SHA256': Lib(lib_SHA256.loadLibrary, () => lib_SHA256.main()), + 'SimdInt32x4': Lib(lib_SimdInt32x4.loadLibrary, () => lib_SimdInt32x4.main()), 'SkeletalAnimation': Lib( lib_SkeletalAnimation.loadLibrary, () => lib_SkeletalAnimation.main(), diff --git a/benchmarks/SimdInt32x4/dart/SimdInt32x4.dart b/benchmarks/SimdInt32x4/dart/SimdInt32x4.dart new file mode 100644 index 00000000000..bac0697ff28 --- /dev/null +++ b/benchmarks/SimdInt32x4/dart/SimdInt32x4.dart @@ -0,0 +1,177 @@ +// 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. + +// Regression benchmark for https://github.com/dart-lang/sdk/issues/63217 and +// https://github.com/dart-lang/sdk/issues/53662: the five binary operators on +// Int32x4 (+, -, |, &, ^) were not specialized in AOT mode, so `Int32x4List` +// loops fell back to boxed runtime calls that ran 10-70x slower than the +// hand-written scalar version or than the JIT. +// +// For every operator there is a scalar variant and a SIMD variant over the +// same Uint32List buffer, so the benchmark suite exposes both the absolute +// cost of each SIMD op and its speedup over the scalar baseline. + +import 'dart:typed_data'; + +import 'package:benchmark_harness/benchmark_harness.dart'; + +const int words = 2048; + +abstract class SimdBench extends BenchmarkBase { + SimdBench(String name) : super('SimdInt32x4.$name'); + + late final Uint32List a; + late final Uint32List b; + + @override + void setup() { + a = Uint32List(words); + b = Uint32List(words); + for (int i = 0; i < words; i++) { + a[i] = 0xA5A5A5A5 ^ i; + b[i] = 0x5A5A5A5A ^ (i * 31); + } + } +} + +class OrScalar extends SimdBench { + OrScalar() : super('orScalar'); + @override + void run() { + final n = a.length; + for (int i = 0; i < n; i++) { + a[i] |= b[i]; + } + } +} + +class OrSimd extends SimdBench { + OrSimd() : super('orSimd'); + @override + void run() { + final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2); + final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2); + for (int j = 0; j < la.length; j++) { + la[j] = la[j] | lb[j]; + } + } +} + +class AndScalar extends SimdBench { + AndScalar() : super('andScalar'); + @override + void run() { + final n = a.length; + for (int i = 0; i < n; i++) { + a[i] &= b[i]; + } + } +} + +class AndSimd extends SimdBench { + AndSimd() : super('andSimd'); + @override + void run() { + final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2); + final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2); + for (int j = 0; j < la.length; j++) { + la[j] = la[j] & lb[j]; + } + } +} + +class XorScalar extends SimdBench { + XorScalar() : super('xorScalar'); + @override + void run() { + final n = a.length; + for (int i = 0; i < n; i++) { + a[i] ^= b[i]; + } + } +} + +class XorSimd extends SimdBench { + XorSimd() : super('xorSimd'); + @override + void run() { + final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2); + final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2); + for (int j = 0; j < la.length; j++) { + la[j] = la[j] ^ lb[j]; + } + } +} + +class AddScalar extends SimdBench { + AddScalar() : super('addScalar'); + @override + void run() { + final n = a.length; + for (int i = 0; i < n; i++) { + a[i] = a[i] + b[i]; + } + } +} + +class AddSimd extends SimdBench { + AddSimd() : super('addSimd'); + @override + void run() { + final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2); + final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2); + for (int j = 0; j < la.length; j++) { + la[j] = la[j] + lb[j]; + } + } +} + +class SubScalar extends SimdBench { + SubScalar() : super('subScalar'); + @override + void run() { + final n = a.length; + for (int i = 0; i < n; i++) { + a[i] = a[i] - b[i]; + } + } +} + +class SubSimd extends SimdBench { + SubSimd() : super('subSimd'); + @override + void run() { + final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2); + final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2); + for (int j = 0; j < la.length; j++) { + la[j] = la[j] - lb[j]; + } + } +} + +void main() { + final benchmarks = [ + OrScalar.new, + OrSimd.new, + AndScalar.new, + AndSimd.new, + XorScalar.new, + XorSimd.new, + AddScalar.new, + AddSimd.new, + SubScalar.new, + SubSimd.new, + ]; + + for (final bm in benchmarks) { + bm() + ..setup() + ..run() + ..run(); + } + + for (final bm in benchmarks) { + bm().report(); + } +} diff --git a/runtime/vm/compiler/backend/il.cc b/runtime/vm/compiler/backend/il.cc index 10ad89da01e..e630c400f21 100644 --- a/runtime/vm/compiler/backend/il.cc +++ b/runtime/vm/compiler/backend/il.cc @@ -8604,6 +8604,16 @@ SimdOpInstr::Kind SimdOpInstr::KindForOperator(MethodRecognizer::Kind kind) { return SimdOpInstr::kFloat64x2Add; case MethodRecognizer::kFloat64x2Sub: return SimdOpInstr::kFloat64x2Sub; + case MethodRecognizer::kInt32x4Add: + return SimdOpInstr::kInt32x4Add; + case MethodRecognizer::kInt32x4Sub: + return SimdOpInstr::kInt32x4Sub; + case MethodRecognizer::kInt32x4BitAnd: + return SimdOpInstr::kInt32x4BitAnd; + case MethodRecognizer::kInt32x4BitOr: + return SimdOpInstr::kInt32x4BitOr; + case MethodRecognizer::kInt32x4BitXor: + return SimdOpInstr::kInt32x4BitXor; default: break; } @@ -8626,6 +8636,11 @@ SimdOpInstr* SimdOpInstr::CreateFromCall(Zone* zone, case MethodRecognizer::kFloat64x2Div: case MethodRecognizer::kFloat64x2Add: case MethodRecognizer::kFloat64x2Sub: + case MethodRecognizer::kInt32x4Add: + case MethodRecognizer::kInt32x4Sub: + case MethodRecognizer::kInt32x4BitAnd: + case MethodRecognizer::kInt32x4BitOr: + case MethodRecognizer::kInt32x4BitXor: op = new (zone) SimdOpInstr(KindForOperator(kind), call->deopt_id()); break; #if defined(TARGET_ARCH_IA32) || defined(TARGET_ARCH_X64) diff --git a/runtime/vm/compiler/call_specializer.cc b/runtime/vm/compiler/call_specializer.cc index ff48b84b163..7735c161da9 100644 --- a/runtime/vm/compiler/call_specializer.cc +++ b/runtime/vm/compiler/call_specializer.cc @@ -3027,6 +3027,11 @@ static bool InlineSimdOp(FlowGraph* flow_graph, case MethodRecognizer::kFloat64x2Div: case MethodRecognizer::kFloat64x2Add: case MethodRecognizer::kFloat64x2Sub: + case MethodRecognizer::kInt32x4Add: + case MethodRecognizer::kInt32x4Sub: + case MethodRecognizer::kInt32x4BitAnd: + case MethodRecognizer::kInt32x4BitOr: + case MethodRecognizer::kInt32x4BitXor: *last = SimdOpInstr::CreateFromCall(Z, kind, receiver, call); if (CompilerState::Current().is_aot()) { // Add null-checks in case of the arguments are known to be compatible @@ -3415,6 +3420,11 @@ bool CallSpecializer::TryInlineRecognizedMethod( case MethodRecognizer::kFloat64x2Div: case MethodRecognizer::kFloat64x2Add: case MethodRecognizer::kFloat64x2Sub: + case MethodRecognizer::kInt32x4Add: + case MethodRecognizer::kInt32x4Sub: + case MethodRecognizer::kInt32x4BitAnd: + case MethodRecognizer::kInt32x4BitOr: + case MethodRecognizer::kInt32x4BitXor: return InlineSimdOp(flow_graph, is_dynamic_call, call, receiver, kind, graph_entry, entry, last, result); diff --git a/runtime/vm/compiler/graph_intrinsifier.cc b/runtime/vm/compiler/graph_intrinsifier.cc index 41371aadae1..c8437e777d2 100644 --- a/runtime/vm/compiler/graph_intrinsifier.cc +++ b/runtime/vm/compiler/graph_intrinsifier.cc @@ -353,7 +353,7 @@ static bool BuildSimdOp(FlowGraph* flow_graph, intptr_t cid, Token::Kind kind) { VerifyParameterIsBoxed(&builder, 1); Cids* value_check = Cids::CreateMonomorphic(zone, cid); - // Check argument. Receiver (left) is known to be a Float32x4. + // Check argument. Receiver (left) is known to match cid by dispatch. builder.AddInstruction(new CheckClassInstr(new Value(right), DeoptId::kNone, *value_check, builder.Source())); Definition* left_simd = builder.AddUnboxInstr( @@ -387,6 +387,26 @@ bool GraphIntrinsifier::Build_Float32x4Add(FlowGraph* flow_graph) { return BuildSimdOp(flow_graph, kFloat32x4Cid, Token::kADD); } +bool GraphIntrinsifier::Build_Int32x4Add(FlowGraph* flow_graph) { + return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kADD); +} + +bool GraphIntrinsifier::Build_Int32x4Sub(FlowGraph* flow_graph) { + return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kSUB); +} + +bool GraphIntrinsifier::Build_Int32x4BitAnd(FlowGraph* flow_graph) { + return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kBIT_AND); +} + +bool GraphIntrinsifier::Build_Int32x4BitOr(FlowGraph* flow_graph) { + return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kBIT_OR); +} + +bool GraphIntrinsifier::Build_Int32x4BitXor(FlowGraph* flow_graph) { + return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kBIT_XOR); +} + bool GraphIntrinsifier::Build_Float64x2Mul(FlowGraph* flow_graph) { return BuildSimdOp(flow_graph, kFloat64x2Cid, Token::kMUL); } diff --git a/runtime/vm/compiler/recognized_methods_list.h b/runtime/vm/compiler/recognized_methods_list.h index 34d9da9a59e..d530b7f56d3 100644 --- a/runtime/vm/compiler/recognized_methods_list.h +++ b/runtime/vm/compiler/recognized_methods_list.h @@ -664,6 +664,11 @@ namespace dart { V(TypedDataLibrary, _Float32x4, /, Float32x4Div, 0xc08217a2) \ V(TypedDataLibrary, _Float32x4, -, Float32x4Sub, 0xdd15548a) \ V(TypedDataLibrary, _Float32x4, +, Float32x4Add, 0xb7dc8a19) \ + V(TypedDataLibrary, _Int32x4, +, Int32x4Add, 0xe9107b19) \ + V(TypedDataLibrary, _Int32x4, -, Int32x4Sub, 0x0e49458a) \ + V(TypedDataLibrary, _Int32x4, |, Int32x4BitOr, 0xf30a0ef5) \ + V(TypedDataLibrary, _Int32x4, &, Int32x4BitAnd, 0x0da1c43d) \ + V(TypedDataLibrary, _Int32x4, ^, Int32x4BitXor, 0x0c49417c) \ V(TypedDataLibrary, _Float64x2, *, Float64x2Mul, 0x37439ec6) \ V(TypedDataLibrary, _Float64x2, /, Float64x2Div, 0x12925562) \ V(TypedDataLibrary, _Float64x2, -, Float64x2Sub, 0x2f258e89) \ diff --git a/sdk/lib/_internal/vm/lib/typed_data_patch.dart b/sdk/lib/_internal/vm/lib/typed_data_patch.dart index ce20269b1e7..ee5a9fdef0f 100644 --- a/sdk/lib/_internal/vm/lib/typed_data_patch.dart +++ b/sdk/lib/_internal/vm/lib/typed_data_patch.dart @@ -3996,14 +3996,24 @@ class Int32x4 { @pragma('vm:deeply-immutable') @pragma("vm:entry-point") final class _Int32x4 implements Int32x4 { + @pragma("vm:recognized", "graph-intrinsic") + @pragma("vm:exact-result-type", _Int32x4) @pragma("vm:external-name", "Int32x4_or") external Int32x4 operator |(Int32x4 other); + @pragma("vm:recognized", "graph-intrinsic") + @pragma("vm:exact-result-type", _Int32x4) @pragma("vm:external-name", "Int32x4_and") external Int32x4 operator &(Int32x4 other); + @pragma("vm:recognized", "graph-intrinsic") + @pragma("vm:exact-result-type", _Int32x4) @pragma("vm:external-name", "Int32x4_xor") external Int32x4 operator ^(Int32x4 other); + @pragma("vm:recognized", "graph-intrinsic") + @pragma("vm:exact-result-type", _Int32x4) @pragma("vm:external-name", "Int32x4_add") external Int32x4 operator +(Int32x4 other); + @pragma("vm:recognized", "graph-intrinsic") + @pragma("vm:exact-result-type", _Int32x4) @pragma("vm:external-name", "Int32x4_sub") external Int32x4 operator -(Int32x4 other); @pragma("vm:external-name", "Int32x4_getX")