Skip to content

Commit b74add4

Browse files
modulovaluedart-scoped@luci-project-accounts.iam.gserviceaccount.com
authored andcommitted
[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: #53662 Bug: #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>
1 parent 41aa8d4 commit b74add4

8 files changed

Lines changed: 242 additions & 1 deletion

File tree

‎benchmarks/Omnibus/dart/Omnibus.dart‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import '../../SHA1/dart/sha1.dart' as lib_SHA1;
2525
import '../../SHA256/dart/sha256.dart' as lib_SHA256;
2626
import '../../SkeletalAnimation/dart/SkeletalAnimation.dart'
2727
as lib_SkeletalAnimation;
28+
import '../../SimdInt32x4/dart/SimdInt32x4.dart' as lib_SimdInt32x4;
2829
import '../../SkeletalAnimationSIMD/dart/SkeletalAnimationSIMD.dart'
2930
as lib_SkeletalAnimationSIMD;
3031
import '../../SwitchFSM/dart/SwitchFSM.dart' as lib_SwitchFSM;
@@ -45,6 +46,7 @@ final Map<String, Function()> benchmarks = {
4546
'RuntimeType': lib_RuntimeType.main,
4647
'SHA1': lib_SHA1.main,
4748
'SHA256': lib_SHA256.main,
49+
'SimdInt32x4': lib_SimdInt32x4.main,
4850
'SkeletalAnimation': lib_SkeletalAnimation.main,
4951
'SkeletalAnimationSIMD': lib_SkeletalAnimationSIMD.main,
5052
'SwitchFSM': lib_SwitchFSM.main,

‎benchmarks/OmnibusDeferred/dart/OmnibusDeferred.dart‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import '../../RecordCollections/dart/RecordCollections.dart'
2424
import '../../RuntimeType/dart/RuntimeType.dart' deferred as lib_RuntimeType;
2525
import '../../SHA1/dart/sha1.dart' deferred as lib_SHA1;
2626
import '../../SHA256/dart/sha256.dart' deferred as lib_SHA256;
27+
import '../../SimdInt32x4/dart/SimdInt32x4.dart' deferred as lib_SimdInt32x4;
2728
import '../../SkeletalAnimation/dart/SkeletalAnimation.dart'
2829
deferred as lib_SkeletalAnimation;
2930
import '../../SkeletalAnimationSIMD/dart/SkeletalAnimationSIMD.dart'
@@ -61,6 +62,7 @@ final Map<String, Lib> benchmarks = {
6162
'RuntimeType': Lib(lib_RuntimeType.loadLibrary, () => lib_RuntimeType.main()),
6263
'SHA1': Lib(lib_SHA1.loadLibrary, () => lib_SHA1.main()),
6364
'SHA256': Lib(lib_SHA256.loadLibrary, () => lib_SHA256.main()),
65+
'SimdInt32x4': Lib(lib_SimdInt32x4.loadLibrary, () => lib_SimdInt32x4.main()),
6466
'SkeletalAnimation': Lib(
6567
lib_SkeletalAnimation.loadLibrary,
6668
() => lib_SkeletalAnimation.main(),
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// Regression benchmark for https://github.com/dart-lang/sdk/issues/63217 and
6+
// https://github.com/dart-lang/sdk/issues/53662: the five binary operators on
7+
// Int32x4 (+, -, |, &, ^) were not specialized in AOT mode, so `Int32x4List`
8+
// loops fell back to boxed runtime calls that ran 10-70x slower than the
9+
// hand-written scalar version or than the JIT.
10+
//
11+
// For every operator there is a scalar variant and a SIMD variant over the
12+
// same Uint32List buffer, so the benchmark suite exposes both the absolute
13+
// cost of each SIMD op and its speedup over the scalar baseline.
14+
15+
import 'dart:typed_data';
16+
17+
import 'package:benchmark_harness/benchmark_harness.dart';
18+
19+
const int words = 2048;
20+
21+
abstract class SimdBench extends BenchmarkBase {
22+
SimdBench(String name) : super('SimdInt32x4.$name');
23+
24+
late final Uint32List a;
25+
late final Uint32List b;
26+
27+
@override
28+
void setup() {
29+
a = Uint32List(words);
30+
b = Uint32List(words);
31+
for (int i = 0; i < words; i++) {
32+
a[i] = 0xA5A5A5A5 ^ i;
33+
b[i] = 0x5A5A5A5A ^ (i * 31);
34+
}
35+
}
36+
}
37+
38+
class OrScalar extends SimdBench {
39+
OrScalar() : super('orScalar');
40+
@override
41+
void run() {
42+
final n = a.length;
43+
for (int i = 0; i < n; i++) {
44+
a[i] |= b[i];
45+
}
46+
}
47+
}
48+
49+
class OrSimd extends SimdBench {
50+
OrSimd() : super('orSimd');
51+
@override
52+
void run() {
53+
final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2);
54+
final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2);
55+
for (int j = 0; j < la.length; j++) {
56+
la[j] = la[j] | lb[j];
57+
}
58+
}
59+
}
60+
61+
class AndScalar extends SimdBench {
62+
AndScalar() : super('andScalar');
63+
@override
64+
void run() {
65+
final n = a.length;
66+
for (int i = 0; i < n; i++) {
67+
a[i] &= b[i];
68+
}
69+
}
70+
}
71+
72+
class AndSimd extends SimdBench {
73+
AndSimd() : super('andSimd');
74+
@override
75+
void run() {
76+
final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2);
77+
final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2);
78+
for (int j = 0; j < la.length; j++) {
79+
la[j] = la[j] & lb[j];
80+
}
81+
}
82+
}
83+
84+
class XorScalar extends SimdBench {
85+
XorScalar() : super('xorScalar');
86+
@override
87+
void run() {
88+
final n = a.length;
89+
for (int i = 0; i < n; i++) {
90+
a[i] ^= b[i];
91+
}
92+
}
93+
}
94+
95+
class XorSimd extends SimdBench {
96+
XorSimd() : super('xorSimd');
97+
@override
98+
void run() {
99+
final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2);
100+
final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2);
101+
for (int j = 0; j < la.length; j++) {
102+
la[j] = la[j] ^ lb[j];
103+
}
104+
}
105+
}
106+
107+
class AddScalar extends SimdBench {
108+
AddScalar() : super('addScalar');
109+
@override
110+
void run() {
111+
final n = a.length;
112+
for (int i = 0; i < n; i++) {
113+
a[i] = a[i] + b[i];
114+
}
115+
}
116+
}
117+
118+
class AddSimd extends SimdBench {
119+
AddSimd() : super('addSimd');
120+
@override
121+
void run() {
122+
final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2);
123+
final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2);
124+
for (int j = 0; j < la.length; j++) {
125+
la[j] = la[j] + lb[j];
126+
}
127+
}
128+
}
129+
130+
class SubScalar extends SimdBench {
131+
SubScalar() : super('subScalar');
132+
@override
133+
void run() {
134+
final n = a.length;
135+
for (int i = 0; i < n; i++) {
136+
a[i] = a[i] - b[i];
137+
}
138+
}
139+
}
140+
141+
class SubSimd extends SimdBench {
142+
SubSimd() : super('subSimd');
143+
@override
144+
void run() {
145+
final la = Int32x4List.view(a.buffer, a.offsetInBytes, a.length >> 2);
146+
final lb = Int32x4List.view(b.buffer, b.offsetInBytes, b.length >> 2);
147+
for (int j = 0; j < la.length; j++) {
148+
la[j] = la[j] - lb[j];
149+
}
150+
}
151+
}
152+
153+
void main() {
154+
final benchmarks = <BenchmarkBase Function()>[
155+
OrScalar.new,
156+
OrSimd.new,
157+
AndScalar.new,
158+
AndSimd.new,
159+
XorScalar.new,
160+
XorSimd.new,
161+
AddScalar.new,
162+
AddSimd.new,
163+
SubScalar.new,
164+
SubSimd.new,
165+
];
166+
167+
for (final bm in benchmarks) {
168+
bm()
169+
..setup()
170+
..run()
171+
..run();
172+
}
173+
174+
for (final bm in benchmarks) {
175+
bm().report();
176+
}
177+
}

‎runtime/vm/compiler/backend/il.cc‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8604,6 +8604,16 @@ SimdOpInstr::Kind SimdOpInstr::KindForOperator(MethodRecognizer::Kind kind) {
86048604
return SimdOpInstr::kFloat64x2Add;
86058605
case MethodRecognizer::kFloat64x2Sub:
86068606
return SimdOpInstr::kFloat64x2Sub;
8607+
case MethodRecognizer::kInt32x4Add:
8608+
return SimdOpInstr::kInt32x4Add;
8609+
case MethodRecognizer::kInt32x4Sub:
8610+
return SimdOpInstr::kInt32x4Sub;
8611+
case MethodRecognizer::kInt32x4BitAnd:
8612+
return SimdOpInstr::kInt32x4BitAnd;
8613+
case MethodRecognizer::kInt32x4BitOr:
8614+
return SimdOpInstr::kInt32x4BitOr;
8615+
case MethodRecognizer::kInt32x4BitXor:
8616+
return SimdOpInstr::kInt32x4BitXor;
86078617
default:
86088618
break;
86098619
}
@@ -8626,6 +8636,11 @@ SimdOpInstr* SimdOpInstr::CreateFromCall(Zone* zone,
86268636
case MethodRecognizer::kFloat64x2Div:
86278637
case MethodRecognizer::kFloat64x2Add:
86288638
case MethodRecognizer::kFloat64x2Sub:
8639+
case MethodRecognizer::kInt32x4Add:
8640+
case MethodRecognizer::kInt32x4Sub:
8641+
case MethodRecognizer::kInt32x4BitAnd:
8642+
case MethodRecognizer::kInt32x4BitOr:
8643+
case MethodRecognizer::kInt32x4BitXor:
86298644
op = new (zone) SimdOpInstr(KindForOperator(kind), call->deopt_id());
86308645
break;
86318646
#if defined(TARGET_ARCH_IA32) || defined(TARGET_ARCH_X64)

‎runtime/vm/compiler/call_specializer.cc‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3027,6 +3027,11 @@ static bool InlineSimdOp(FlowGraph* flow_graph,
30273027
case MethodRecognizer::kFloat64x2Div:
30283028
case MethodRecognizer::kFloat64x2Add:
30293029
case MethodRecognizer::kFloat64x2Sub:
3030+
case MethodRecognizer::kInt32x4Add:
3031+
case MethodRecognizer::kInt32x4Sub:
3032+
case MethodRecognizer::kInt32x4BitAnd:
3033+
case MethodRecognizer::kInt32x4BitOr:
3034+
case MethodRecognizer::kInt32x4BitXor:
30303035
*last = SimdOpInstr::CreateFromCall(Z, kind, receiver, call);
30313036
if (CompilerState::Current().is_aot()) {
30323037
// Add null-checks in case of the arguments are known to be compatible
@@ -3415,6 +3420,11 @@ bool CallSpecializer::TryInlineRecognizedMethod(
34153420
case MethodRecognizer::kFloat64x2Div:
34163421
case MethodRecognizer::kFloat64x2Add:
34173422
case MethodRecognizer::kFloat64x2Sub:
3423+
case MethodRecognizer::kInt32x4Add:
3424+
case MethodRecognizer::kInt32x4Sub:
3425+
case MethodRecognizer::kInt32x4BitAnd:
3426+
case MethodRecognizer::kInt32x4BitOr:
3427+
case MethodRecognizer::kInt32x4BitXor:
34183428
return InlineSimdOp(flow_graph, is_dynamic_call, call, receiver, kind,
34193429
graph_entry, entry, last, result);
34203430

‎runtime/vm/compiler/graph_intrinsifier.cc‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ static bool BuildSimdOp(FlowGraph* flow_graph, intptr_t cid, Token::Kind kind) {
353353
VerifyParameterIsBoxed(&builder, 1);
354354

355355
Cids* value_check = Cids::CreateMonomorphic(zone, cid);
356-
// Check argument. Receiver (left) is known to be a Float32x4.
356+
// Check argument. Receiver (left) is known to match cid by dispatch.
357357
builder.AddInstruction(new CheckClassInstr(new Value(right), DeoptId::kNone,
358358
*value_check, builder.Source()));
359359
Definition* left_simd = builder.AddUnboxInstr(
@@ -387,6 +387,26 @@ bool GraphIntrinsifier::Build_Float32x4Add(FlowGraph* flow_graph) {
387387
return BuildSimdOp(flow_graph, kFloat32x4Cid, Token::kADD);
388388
}
389389

390+
bool GraphIntrinsifier::Build_Int32x4Add(FlowGraph* flow_graph) {
391+
return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kADD);
392+
}
393+
394+
bool GraphIntrinsifier::Build_Int32x4Sub(FlowGraph* flow_graph) {
395+
return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kSUB);
396+
}
397+
398+
bool GraphIntrinsifier::Build_Int32x4BitAnd(FlowGraph* flow_graph) {
399+
return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kBIT_AND);
400+
}
401+
402+
bool GraphIntrinsifier::Build_Int32x4BitOr(FlowGraph* flow_graph) {
403+
return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kBIT_OR);
404+
}
405+
406+
bool GraphIntrinsifier::Build_Int32x4BitXor(FlowGraph* flow_graph) {
407+
return BuildSimdOp(flow_graph, kInt32x4Cid, Token::kBIT_XOR);
408+
}
409+
390410
bool GraphIntrinsifier::Build_Float64x2Mul(FlowGraph* flow_graph) {
391411
return BuildSimdOp(flow_graph, kFloat64x2Cid, Token::kMUL);
392412
}

‎runtime/vm/compiler/recognized_methods_list.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,11 @@ namespace dart {
664664
V(TypedDataLibrary, _Float32x4, /, Float32x4Div, 0xc08217a2) \
665665
V(TypedDataLibrary, _Float32x4, -, Float32x4Sub, 0xdd15548a) \
666666
V(TypedDataLibrary, _Float32x4, +, Float32x4Add, 0xb7dc8a19) \
667+
V(TypedDataLibrary, _Int32x4, +, Int32x4Add, 0xe9107b19) \
668+
V(TypedDataLibrary, _Int32x4, -, Int32x4Sub, 0x0e49458a) \
669+
V(TypedDataLibrary, _Int32x4, |, Int32x4BitOr, 0xf30a0ef5) \
670+
V(TypedDataLibrary, _Int32x4, &, Int32x4BitAnd, 0x0da1c43d) \
671+
V(TypedDataLibrary, _Int32x4, ^, Int32x4BitXor, 0x0c49417c) \
667672
V(TypedDataLibrary, _Float64x2, *, Float64x2Mul, 0x37439ec6) \
668673
V(TypedDataLibrary, _Float64x2, /, Float64x2Div, 0x12925562) \
669674
V(TypedDataLibrary, _Float64x2, -, Float64x2Sub, 0x2f258e89) \

‎sdk/lib/_internal/vm/lib/typed_data_patch.dart‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3996,14 +3996,24 @@ class Int32x4 {
39963996
@pragma('vm:deeply-immutable')
39973997
@pragma("vm:entry-point")
39983998
final class _Int32x4 implements Int32x4 {
3999+
@pragma("vm:recognized", "graph-intrinsic")
4000+
@pragma("vm:exact-result-type", _Int32x4)
39994001
@pragma("vm:external-name", "Int32x4_or")
40004002
external Int32x4 operator |(Int32x4 other);
4003+
@pragma("vm:recognized", "graph-intrinsic")
4004+
@pragma("vm:exact-result-type", _Int32x4)
40014005
@pragma("vm:external-name", "Int32x4_and")
40024006
external Int32x4 operator &(Int32x4 other);
4007+
@pragma("vm:recognized", "graph-intrinsic")
4008+
@pragma("vm:exact-result-type", _Int32x4)
40034009
@pragma("vm:external-name", "Int32x4_xor")
40044010
external Int32x4 operator ^(Int32x4 other);
4011+
@pragma("vm:recognized", "graph-intrinsic")
4012+
@pragma("vm:exact-result-type", _Int32x4)
40054013
@pragma("vm:external-name", "Int32x4_add")
40064014
external Int32x4 operator +(Int32x4 other);
4015+
@pragma("vm:recognized", "graph-intrinsic")
4016+
@pragma("vm:exact-result-type", _Int32x4)
40074017
@pragma("vm:external-name", "Int32x4_sub")
40084018
external Int32x4 operator -(Int32x4 other);
40094019
@pragma("vm:external-name", "Int32x4_getX")

0 commit comments

Comments
 (0)