[vm/compiler] Specialize Int32x4 operators in AOT.

The five binary operators on Int32x4 (+, -, |, &, ^) were never added
to the recognized-method list as graph intrinsics, so calls to them
were left as runtime calls through external-name bodies. In JIT the
call specializer picked kInt32x4Cid from IC feedback and emitted a
native SimdOpInstr, but AOT has no IC feedback and therefore fell back
to boxed calls, making Int32x4List inner loops 10-70x slower than both
the JIT version and a hand-written scalar equivalent.

This CL wires the same specialization paths that already exist for
Float32x4.+,-,*,/:
  - recognize the five operators as graph intrinsics and mark them
    with `@pragma("vm:recognized", "graph-intrinsic")` plus an
    exact-result-type pragma;
  - add Build_Int32x4{Add,Sub,BitAnd,BitOr,BitXor} helpers that
    delegate to the existing BuildSimdOp;
  - extend SimdOpInstr::KindForOperator and CreateFromCall;
  - extend CallSpecializer::InlineSimdOp and TryInlineRecognizedMethod
    so the non-speculative null-check path used for Float32x4 operators
    in AOT also applies here.

Measured on macOS arm64 (M-series), `dart compile exe`:

  Issue 63217 orSimd      : 12.58 -> 0.32 us/iter  (39x)
  Issue 63217 andNotSimd  : 23.51 -> 0.34 us/iter  (69x)
  Issue 53662 mandelbrot  : 4038.5 -> 55.5 ms       (72x)

A new benchmark benchmarks/SimdInt32x4 exercises all five operators
with a scalar and a SIMD variant so the specialization stays covered
by the benchmark bots; it is registered in Omnibus and OmnibusDeferred.

Existing tests/lib/typed_data/simd_*_test.dart still pass in JIT and
AOT.

TEST=tests/lib/typed_data/int32x4_arithmetic_test; benchmarks/SimdInt32x4
Bug: https://github.com/dart-lang/sdk/issues/53662
Bug: https://github.com/dart-lang/sdk/issues/63217
Change-Id: I9b76ab4fff228ff1a5e3d3c86f4bfc059e66a49a
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/497000
Reviewed-by: Slava Egorov <vegorov@google.com>
Commit-Queue: Slava Egorov <vegorov@google.com>
Auto-Submit: Modestas Valauskas <valauskasmodestas@gmail.com>
Reviewed-by: Alexander Aprelev <aam@google.com>
This commit is contained in:
Modestas Valauskas
2026-04-22 00:01:52 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 41aa8d42bc
commit b74add4813
8 changed files with 242 additions and 1 deletions
+2
View File
@@ -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<String, Function()> 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,
@@ -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<String, Lib> 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(),
@@ -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 = <BenchmarkBase Function()>[
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();
}
}
+15
View File
@@ -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)
+10
View File
@@ -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);
+21 -1
View File
@@ -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);
}
@@ -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) \
@@ -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")