[compiler] Inline Array.prototype.at in JSCallReducer

Bug: v8:12865
Change-Id: I539a5b0a9c3c78ef9a767de75b71dd06de337d9a
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3647351
Reviewed-by: Maya Lekova <mslekova@chromium.org>
Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#80740}
This commit is contained in:
Darius M 2022-05-24 18:50:31 +02:00 committed by V8 LUCI CQ
parent 47d8833875
commit 702f0ff111
5 changed files with 437 additions and 0 deletions

View File

@ -718,6 +718,9 @@ class IteratingArrayBuiltinReducerAssembler : public JSCallReducerAssembler {
MapInference* inference, const bool has_stability_dependency,
ElementsKind kind, const SharedFunctionInfoRef& shared,
const NativeContextRef& native_context, ArrayEverySomeVariant variant);
TNode<Object> ReduceArrayPrototypeAt(ZoneVector<ElementsKind> kinds,
bool needs_fallback_builtin_call,
Node* receiver_kind);
TNode<Object> ReduceArrayPrototypeIndexOfIncludes(
ElementsKind kind, ArrayIndexOfIncludesVariant variant);
@ -1326,6 +1329,83 @@ TNode<String> JSCallReducerAssembler::ReduceStringPrototypeSlice() {
.Value();
}
TNode<Object> IteratingArrayBuiltinReducerAssembler::ReduceArrayPrototypeAt(
ZoneVector<ElementsKind> kinds, bool needs_fallback_builtin_call,
Node* receiver_kind) {
TNode<JSArray> receiver = ReceiverInputAs<JSArray>();
TNode<Object> index = ArgumentOrZero(0);
TNode<Number> index_num = CheckSmi(index);
TNode<FixedArrayBase> elements = LoadElements(receiver);
auto out = MakeLabel(MachineRepresentation::kTagged);
for (ElementsKind kind : kinds) {
auto correct_map_label = MakeLabel(), wrong_map_label = MakeLabel();
Branch(NumberEqual(TNode<Number>::UncheckedCast(receiver_kind),
NumberConstant(kind)),
&correct_map_label, &wrong_map_label);
Bind(&correct_map_label);
TNode<Number> length = LoadJSArrayLength(receiver, kind);
// If index is less than 0, then subtract from length.
TNode<Boolean> cond = NumberLessThan(index_num, ZeroConstant());
TNode<Number> real_index_num =
SelectIf<Number>(cond)
.Then(_ { return NumberAdd(length, index_num); })
.Else(_ { return index_num; })
.ExpectTrue() // Most common usage should be .at(-1)
.Value();
// Bound checking.
GotoIf(NumberLessThan(real_index_num, ZeroConstant()), &out,
UndefinedConstant());
GotoIfNot(NumberLessThan(real_index_num, length), &out,
UndefinedConstant());
// Retrieving element at index.
TNode<Object> element = LoadElement<Object>(
AccessBuilder::ForFixedArrayElement(kind), elements, real_index_num);
if (IsHoleyElementsKind(kind)) {
// This case is needed in particular for HOLEY_DOUBLE_ELEMENTS: raw
// doubles are stored in the FixedDoubleArray, and need to be converted to
// HeapNumber or to Smi so that this function can return an Object. The
// automatic converstion performed by
// RepresentationChanger::GetTaggedRepresentationFor does not handle
// holes, so we convert manually a potential hole here.
element = TryConvertHoleToUndefined(element, kind);
}
Goto(&out, element);
Bind(&wrong_map_label);
}
if (needs_fallback_builtin_call) {
JSCallNode n(node_ptr());
CallParameters const& p = n.Parameters();
// We set SpeculationMode to kDisallowSpeculation to avoid infinite
// recursion on the node we're creating (since, after all, it's calling
// Array.Prototype.at).
const Operator* op = javascript()->Call(
JSCallNode::ArityForArgc(1), p.frequency(), p.feedback(),
ConvertReceiverMode::kNotNullOrUndefined,
SpeculationMode::kDisallowSpeculation, CallFeedbackRelation::kTarget);
Node* fallback_builtin = node_ptr()->InputAt(0);
TNode<Object> res = AddNode<Object>(graph()->NewNode(
op, fallback_builtin, receiver, index, n.feedback_vector(),
ContextInput(), n.frame_state(), effect(), control()));
Goto(&out, res);
} else {
Goto(&out, UndefinedConstant());
}
Bind(&out);
return out.PhiAt<Object>(0);
}
namespace {
struct ForEachFrameStateParams {
@ -4608,6 +4688,8 @@ Reduction JSCallReducer::ReduceJSCall(Node* node,
return ReduceArrayIncludes(node);
case Builtin::kArraySome:
return ReduceArraySome(node, shared);
case Builtin::kArrayPrototypeAt:
return ReduceArrayPrototypeAt(node);
case Builtin::kArrayPrototypePush:
return ReduceArrayPrototypePush(node);
case Builtin::kArrayPrototypePop:
@ -5539,6 +5621,60 @@ void JSCallReducer::CheckIfElementsKind(Node* receiver_elements_kind,
}
}
// ES6 section 23.1.3.1 Array.prototype.at ( )
Reduction JSCallReducer::ReduceArrayPrototypeAt(Node* node) {
if (!FLAG_turbo_inline_array_builtins) return NoChange();
JSCallNode n(node);
CallParameters const& p = n.Parameters();
if (p.speculation_mode() == SpeculationMode::kDisallowSpeculation) {
return NoChange();
}
Node* receiver = n.receiver();
Effect effect = n.effect();
Control control = n.control();
MapInference inference(broker(), receiver, effect);
if (!inference.HaveMaps()) return NoChange();
// Collecting kinds
ZoneVector<ElementsKind> kinds(broker()->zone());
bool needs_fallback_builtin_call = false;
for (const MapRef& map : inference.GetMaps()) {
if (map.supports_fast_array_iteration()) {
ElementsKind kind = map.elements_kind();
// Checking that |kind| isn't already in |kinds|. Using std::find should
// be fast enough since |kinds| can contain at most 4 items.
if (std::find(kinds.begin(), kinds.end(), kind) == kinds.end()) {
kinds.push_back(kind);
}
} else {
needs_fallback_builtin_call = true;
}
}
inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &effect,
control, p.feedback());
if (kinds.empty()) {
// No map in the feedback supports fast iteration. Keeping the builtin call.
return NoChange();
}
if (!dependencies()->DependOnNoElementsProtector()) {
return NoChange();
}
Node* receiver_kind = LoadReceiverElementsKind(receiver, &effect, control);
IteratingArrayBuiltinReducerAssembler a(this, node);
a.InitializeEffectControl(effect, control);
TNode<Object> subgraph = a.ReduceArrayPrototypeAt(
kinds, needs_fallback_builtin_call, receiver_kind);
return ReplaceWithSubgraph(&a, subgraph);
}
// ES6 section 22.1.3.18 Array.prototype.push ( )
Reduction JSCallReducer::ReduceArrayPrototypePush(Node* node) {
JSCallNode n(node);

View File

@ -111,6 +111,7 @@ class V8_EXPORT_PRIVATE JSCallReducer final : public AdvancedReducer {
Reduction ReduceArrayIndexOf(Node* node);
Reduction ReduceArrayIsArray(Node* node);
Reduction ReduceArrayMap(Node* node, const SharedFunctionInfoRef& shared);
Reduction ReduceArrayPrototypeAt(Node* node);
Reduction ReduceArrayPrototypePop(Node* node);
Reduction ReduceArrayPrototypePush(Node* node);
Reduction ReduceArrayPrototypeShift(Node* node);

View File

@ -0,0 +1,104 @@
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const array_at_size = 100;
// SMI array
(() => {
const A = new Array(array_at_size);
for (let i = 0; i < A.length; i++) {
A[i] = i;
}
assert(%HasSmiElements(A), "A should have SMI elements for this test");
// Commonly used to get the last item.
function testArrayAtNeg1() {
return A.at(-1);
}
function testArrayAt0() {
return A.at(0);
}
function testArrayAt20() {
return A.at(20);
}
function testArrayAt80() {
return A.at(80);
}
createSuiteWithWarmup("Array.at(-1)-smi", 1, testArrayAtNeg1);
createSuiteWithWarmup("Array.at(0)-smi", 1, testArrayAt0);
createSuiteWithWarmup("Array.at(20)-smi", 1, testArrayAt20);
createSuiteWithWarmup("Array.at(80)-smi", 1, testArrayAt80);
})();
// Double array
(() => {
const A = new Array(array_at_size);
for (let i = 0; i < A.length; i++) {
A[i] = i + 0.5;
}
assert(%HasDoubleElements(A), "A should have Double elements for this test");
// Commonly used to get the last item.
function testArrayAtNeg1() {
return A.at(-1);
}
function testArrayAt0() {
return A.at(0);
}
function testArrayAt20() {
return A.at(20);
}
function testArrayAt80() {
return A.at(80);
}
createSuiteWithWarmup("Array.at(-1)-double", 1, testArrayAtNeg1);
createSuiteWithWarmup("Array.at(0)-double", 1, testArrayAt0);
createSuiteWithWarmup("Array.at(20)-double", 1, testArrayAt20);
createSuiteWithWarmup("Array.at(80)-double", 1, testArrayAt80);
})();
// Object array
(() => {
const A = new Array(array_at_size);
for (let i = 0; i < A.length; i++) {
A[i] = { ["p" + i] : i};
}
assert(%HasObjectElements(A), "A should have Object elements for this test");
// Commonly used to get the last item.
function testArrayAtNeg1() {
return A.at(-1);
}
function testArrayAt0() {
return A.at(0);
}
function testArrayAt20() {
return A.at(20);
}
function testArrayAt80() {
return A.at(80);
}
createSuiteWithWarmup("Array.at(-1)-object", 1, testArrayAtNeg1);
createSuiteWithWarmup("Array.at(0)-object", 1, testArrayAt0);
createSuiteWithWarmup("Array.at(20)-object", 1, testArrayAt20);
createSuiteWithWarmup("Array.at(80)-object", 1, testArrayAt80);
})();

View File

@ -141,6 +141,7 @@ d8.file.execute('join.js');
d8.file.execute('to-string.js');
d8.file.execute('slice.js');
d8.file.execute('copy-within.js');
d8.file.execute('at.js');
var success = true;

View File

@ -0,0 +1,195 @@
// Copyright 2022 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax --turbo-inline-array-builtins --turbofan
// Flags: --no-always-turbofan
// Out of bounds
(() => {
const arr = [0, 1, 2.5, 3, {}, 5, "a", 7, 8, { "x": 42 }, 10];
function testArrayAt(a, idx) {
return a.at(idx);
}
%PrepareFunctionForOptimization(testArrayAt);
testArrayAt(arr, 2);
assertEquals(arr[2], testArrayAt(arr, 2));
%OptimizeFunctionOnNextCall(testArrayAt);
testArrayAt(arr, 2);
assertOptimized(testArrayAt);
// Checking out of bounds
assertEquals(undefined, testArrayAt(arr, -20));
assertOptimized(testArrayAt);
assertEquals(undefined, testArrayAt(arr, 20));
assertOptimized(testArrayAt);
})();
// Smi array
(() => {
const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function testArrayAt(a, idx) {
return a.at(idx);
}
%PrepareFunctionForOptimization(testArrayAt);
testArrayAt(arr, 2);
assertEquals(arr[2], testArrayAt(arr, 2));
%OptimizeFunctionOnNextCall(testArrayAt);
testArrayAt(arr, 2);
assertOptimized(testArrayAt);
// Positive indices
for (let idx = 0; idx < arr.length; idx++) {
assertEquals(arr[idx], testArrayAt(arr, idx));
assertOptimized(testArrayAt);
}
// Negative indices
for (let idx = 1; idx <= arr.length; idx++) {
assertEquals(arr[arr.length - idx], testArrayAt(arr, -idx));
assertOptimized(testArrayAt);
}
})();
// Double array
(() => {
const arr = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5];
function testArrayAt(a, idx) {
return a.at(idx);
}
%PrepareFunctionForOptimization(testArrayAt);
testArrayAt(arr, 2);
assertEquals(arr[2], testArrayAt(arr, 2));
%OptimizeFunctionOnNextCall(testArrayAt);
testArrayAt(arr, 2);
assertOptimized(testArrayAt);
// Positive indices
for (let idx = 0; idx < arr.length; idx++) {
assertEquals(arr[idx], testArrayAt(arr, idx));
assertOptimized(testArrayAt);
}
// Negative indices
for (let idx = 1; idx <= arr.length; idx++) {
assertEquals(arr[arr.length - idx], testArrayAt(arr, -idx));
assertOptimized(testArrayAt);
}
})();
// Obj array
(() => {
const arr = [0, 1, 2.5, 3, {}, 5, "a", 7, 8, { "x": 42 }, 10];
function testArrayAt(a, idx) {
return a.at(idx);
}
%PrepareFunctionForOptimization(testArrayAt);
testArrayAt(arr, 2);
assertEquals(arr[2], testArrayAt(arr, 2));
%OptimizeFunctionOnNextCall(testArrayAt);
testArrayAt(arr, 2);
assertOptimized(testArrayAt);
// Positive indices
for (let idx = 0; idx < arr.length; idx++) {
assertEquals(arr[idx], testArrayAt(arr, idx));
assertOptimized(testArrayAt);
}
// Negative indices
for (let idx = 1; idx <= arr.length; idx++) {
assertEquals(arr[arr.length - idx], testArrayAt(arr, -idx));
assertOptimized(testArrayAt);
}
})();
// Smi/Double/Obj arrays. This test ensure that:
// - When different kinds are present in the feedback, they all get properly
// optimized: |smis|, |doubles| and |objects| will produce 3 different
// branches in the inlined code.
//
// - The "needs_fallback_builtin_call" mechanism works as intended: while the
// |smis|, |doubles| and |objects| cases will be inlined, |dict| should
// produce a call to the builtin (without deoptimizing).
(() => {
const smis = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const doubles = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5];
const objects = [0, 1, 2.5, 3, {}, 5, "a", 7, 8, { "x": 42 }, 10];
const dict = new Array();
dict[2] = 42;
dict[10000] = 5;
assertEquals(smis.length, doubles.length);
assertEquals(smis.length, objects.length);
function testArrayAt(a, idx) {
return a.at(idx);
}
%PrepareFunctionForOptimization(testArrayAt);
testArrayAt(smis, 2);
testArrayAt(doubles, 2);
testArrayAt(objects, 2);
testArrayAt(dict, 2);
assertEquals(doubles[2], testArrayAt(doubles, 2));
assertEquals(objects[2], testArrayAt(objects, 2));
assertEquals(dict[2], testArrayAt(dict, 2));
assertEquals(smis[2], testArrayAt(smis, 2));
%OptimizeFunctionOnNextCall(testArrayAt);
testArrayAt(smis, 2);
assertOptimized(testArrayAt);
testArrayAt(doubles, 2);
assertOptimized(testArrayAt);
testArrayAt(objects, 2);
assertOptimized(testArrayAt);
testArrayAt(dict, 2);
assertOptimized(testArrayAt);
// Positive indices
for (let idx = 0; idx < smis.length; idx++) {
assertEquals(smis[idx], testArrayAt(smis, idx));
assertOptimized(testArrayAt);
assertEquals(doubles[idx], testArrayAt(doubles, idx));
assertOptimized(testArrayAt);
assertEquals(objects[idx], testArrayAt(objects, idx));
assertOptimized(testArrayAt);
assertEquals(dict[idx], testArrayAt(dict, idx));
assertOptimized(testArrayAt);
}
// Negative indices
for (let idx = 1; idx <= smis.length; idx++) {
assertEquals(smis[smis.length - idx], testArrayAt(smis, -idx));
assertOptimized(testArrayAt);
assertEquals(doubles[doubles.length - idx], testArrayAt(doubles, -idx));
assertOptimized(testArrayAt);
assertEquals(objects[objects.length - idx], testArrayAt(objects, -idx));
assertOptimized(testArrayAt);
assertEquals(dict[dict.length - idx], testArrayAt(dict, -idx));
assertOptimized(testArrayAt);
}
})();
// Checks that accessing holes in double arrays leads to deoptimization.
(() => {
let doubles = Array();
doubles[10] = 4.5;
function testArrayAt(idx) {
return doubles.at(idx);
}
%PrepareFunctionForOptimization(testArrayAt);
testArrayAt(10);
%OptimizeFunctionOnNextCall(testArrayAt);
testArrayAt(10);
assertOptimized(testArrayAt);
assertEquals(doubles[10], testArrayAt(10));
assertOptimized(testArrayAt);
assertEquals(doubles[2], testArrayAt(2));
assertUnoptimized(testArrayAt);
})();
// Checks that accessing holes in SMI array does not lead to deoptimization.
(() => {
let smis = Array();
smis[10] = 4;
function testArrayAt(idx) {
return smis.at(idx);
}
%PrepareFunctionForOptimization(testArrayAt);
testArrayAt(10);
%OptimizeFunctionOnNextCall(testArrayAt);
testArrayAt(10);
assertOptimized(testArrayAt);
assertEquals(smis[10], testArrayAt(10));
assertOptimized(testArrayAt);
assertEquals(smis[2], testArrayAt(2));
assertOptimized(testArrayAt);
})();