Commit 3e43ded9 authored by Benedikt Meurer's avatar Benedikt Meurer Committed by Commit Bot

[turbofan] Escape analysis support for LoadElement with variable index.

This adds support to the escape analysis to allow scalar replacement
of (small) FixedArrays with element accesses where the index is not a
compile time constant. This happens quite often when inlining functions
that operate on variable number of arguments. For example consider this
little piece of code:

```js
function sum(...args) {
  let s = 0;
  for (let i = 0; i < args.length; ++i) s += args[i];
  return s;
}

function sum2(x, y) {
  return sum(x, y);
}
```

This example is made up, of course, but it shows the problem. Let's
assume that TurboFan inlines the function `sum` into it's call site
at `sum2`. Now it has to materialize the `args` array with the two
values `x` and `y`, and iterate through these `args` to sum them up.
The escape analysis pass figures out that `args` doesn't escape (aka
doesn't outlive) the optimized code for `sum2` now, but TurboFan still
needs to materialize the elements backing store for `args` since there's
a `LoadElement(args.elements,i)` in the graph now, and `i` is not a
compile time constant.

However the escape analysis has more information than just that. In
particular the escape analysis knows exactly how many elements a non
escaping object has, based on the fact that the allocation must be
local to the function and that we only track objects with known size.
So in the case above when we get to `args[i]` in the escape analysis
the relevant part of the graph looks something like this:

```
elements = LoadField[elements](args)
length = LoadField[length](args)
index = CheckBounds(i, length)
value = LoadElement(elements, index)
```

In particular the contract here is that `LoadElement(elements,index)`
is guaranteed to have an `index` that is within the valid bounds for
the `elements` (there must be a preceeding `CheckBounds` or some other
guard in optimized code before it). And since `elements` is allocated
inside of the optimized code object, the escape analysis also knows
that `elements` has exactly two elements inside (namely the values of
`x` and `y`). So we can use that information and replace the access
with a `Select(index===0,x,y)` operation instead, which allows us to
scalar replace the `elements`, since there's no escaping use anymore
in the graph.

We do this for the case that the number of elements is 2, as described
above, but also for the case where elements length is one. In case
of 0, we know that the `LoadElement` must be in dead code, but we can't
just mark it for deletion from the graph (to make sure it doesn't block
scalar replacement of non-dead code), so we don't handle this for now.
And for one element it's even easier, since the `LoadElement` has to
yield exactly said element.

We could generalize this to handle arbitrary lengths, but since there's
a cost to arbitrary decision trees here, it's unclear when this is still
beneficial. Another possible solution for length > 2 would be to have
special stack allocation for these backing stores and do variable index
accesses to these stack areas. But that's way beyond the scope of this
isolated change.

This change shows a ~2% improvement on the EarleyBoyer benchmark in
JetStream, since it benefits a lot from not having to materialize these
small arguments backing stores.

Drive-by-fix: Fix JSCreateLowering to properly initialize "elements"
with StoreElement instead of StoreField (which violates the invariant
in TurboFan that fields and elements never alias).

Bug: v8:5267, v8:6200
Change-Id: Idd464a15a81e7c9653c48c814b406eb859841428
Reviewed-on: https://chromium-review.googlesource.com/c/1267935
Commit-Queue: Benedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarTobias Tebbi <tebbi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#56442}
parent 51274d3c
......@@ -500,6 +500,14 @@ int OffsetOfFieldAccess(const Operator* op) {
return access.offset;
}
int OffsetOfElementAt(ElementAccess const& access, int index) {
DCHECK_GE(index, 0);
DCHECK_GE(ElementSizeLog2Of(access.machine_type.representation()),
kPointerSizeLog2);
return access.header_size +
(index << ElementSizeLog2Of(access.machine_type.representation()));
}
Maybe<int> OffsetOfElementsAccess(const Operator* op, Node* index_node) {
DCHECK(op->opcode() == IrOpcode::kLoadElement ||
op->opcode() == IrOpcode::kStoreElement);
......@@ -509,11 +517,7 @@ Maybe<int> OffsetOfElementsAccess(const Operator* op, Node* index_node) {
double min = index_type.Min();
int index = static_cast<int>(min);
if (!(index == min && index == max)) return Nothing<int>();
ElementAccess access = ElementAccessOf(op);
DCHECK_GE(ElementSizeLog2Of(access.machine_type.representation()),
kPointerSizeLog2);
return Just(access.header_size + (index << ElementSizeLog2Of(
access.machine_type.representation())));
return Just(OffsetOfElementAt(ElementAccessOf(op), index));
}
Node* LowerCompareMapsWithoutLoad(Node* checked_map,
......@@ -618,9 +622,56 @@ void ReduceNode(const Operator* op, EscapeAnalysisTracker::Scope* current,
OffsetOfElementsAccess(op, index).To(&offset) &&
vobject->FieldAt(offset).To(&var) && current->Get(var).To(&value)) {
current->SetReplacement(value);
} else {
current->SetEscaped(object);
} else if (vobject && !vobject->HasEscaped()) {
// Compute the known length (aka the number of elements) of {object}
// based on the virtual object information.
ElementAccess const& access = ElementAccessOf(op);
int const length =
(vobject->size() - access.header_size) >>
ElementSizeLog2Of(access.machine_type.representation());
Variable var0, var1;
Node* value0;
Node* value1;
if (length == 1 &&
vobject->FieldAt(OffsetOfElementAt(access, 0)).To(&var) &&
current->Get(var).To(&value)) {
// The {object} has no elements, and we know that the LoadElement
// {index} must be within bounds, thus it must always yield this
// one element of {object}.
current->SetReplacement(value);
break;
} else if (length == 2 &&
vobject->FieldAt(OffsetOfElementAt(access, 0)).To(&var0) &&
current->Get(var0).To(&value0) &&
vobject->FieldAt(OffsetOfElementAt(access, 1)).To(&var1) &&
current->Get(var1).To(&value1)) {
if (value0 && value1) {
// The {object} has exactly two elements, so the LoadElement
// must return one of them (i.e. either the element at index
// 0 or the one at index 1). So we can turn the LoadElement
// into a Select operation instead (still allowing the {object}
// to be scalar replaced). We must however mark the elements
// of the {object} itself as escaping.
Node* check =
jsgraph->graph()->NewNode(jsgraph->simplified()->NumberEqual(),
index, jsgraph->ZeroConstant());
NodeProperties::SetType(check, Type::Boolean());
Node* select = jsgraph->graph()->NewNode(
jsgraph->common()->Select(access.machine_type.representation()),
check, value0, value1);
NodeProperties::SetType(select, access.type);
current->SetReplacement(select);
current->SetEscaped(value0);
current->SetEscaped(value1);
break;
} else {
// If the variables have no values, we have
// not reached the fixed-point yet.
break;
}
}
}
current->SetEscaped(object);
break;
}
case IrOpcode::kTypeGuard: {
......
......@@ -1392,7 +1392,8 @@ Node* JSCreateLowering::AllocateArguments(Node* effect, Node* control,
a.AllocateArray(argument_count, factory()->fixed_array_map());
for (int i = 0; i < argument_count; ++i, ++parameters_it) {
DCHECK_NOT_NULL((*parameters_it).node);
a.Store(AccessBuilder::ForFixedArraySlot(i), (*parameters_it).node);
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(i),
(*parameters_it).node);
}
return a.Finish();
}
......@@ -1422,7 +1423,8 @@ Node* JSCreateLowering::AllocateRestArguments(Node* effect, Node* control,
a.AllocateArray(num_elements, factory()->fixed_array_map());
for (int i = 0; i < num_elements; ++i, ++parameters_it) {
DCHECK_NOT_NULL((*parameters_it).node);
a.Store(AccessBuilder::ForFixedArraySlot(i), (*parameters_it).node);
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(i),
(*parameters_it).node);
}
return a.Finish();
}
......@@ -1459,22 +1461,27 @@ Node* JSCreateLowering::AllocateAliasedArguments(
AllocationBuilder aa(jsgraph(), effect, control);
aa.AllocateArray(argument_count, factory()->fixed_array_map());
for (int i = 0; i < mapped_count; ++i, ++parameters_it) {
aa.Store(AccessBuilder::ForFixedArraySlot(i), jsgraph()->TheHoleConstant());
aa.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(i),
jsgraph()->TheHoleConstant());
}
for (int i = mapped_count; i < argument_count; ++i, ++parameters_it) {
DCHECK_NOT_NULL((*parameters_it).node);
aa.Store(AccessBuilder::ForFixedArraySlot(i), (*parameters_it).node);
aa.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(i),
(*parameters_it).node);
}
Node* arguments = aa.Finish();
// Actually allocate the backing store.
AllocationBuilder a(jsgraph(), arguments, control);
a.AllocateArray(mapped_count + 2, factory()->sloppy_arguments_elements_map());
a.Store(AccessBuilder::ForFixedArraySlot(0), context);
a.Store(AccessBuilder::ForFixedArraySlot(1), arguments);
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(0),
context);
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(1),
arguments);
for (int i = 0; i < mapped_count; ++i) {
int idx = Context::MIN_CONTEXT_SLOTS + parameter_count - 1 - i;
a.Store(AccessBuilder::ForFixedArraySlot(i + 2), jsgraph()->Constant(idx));
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(i + 2),
jsgraph()->Constant(idx));
}
return a.Finish();
}
......@@ -1512,8 +1519,10 @@ Node* JSCreateLowering::AllocateAliasedArguments(
// Actually allocate the backing store.
AllocationBuilder a(jsgraph(), arguments, control);
a.AllocateArray(mapped_count + 2, factory()->sloppy_arguments_elements_map());
a.Store(AccessBuilder::ForFixedArraySlot(0), context);
a.Store(AccessBuilder::ForFixedArraySlot(1), arguments);
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(0),
context);
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(1),
arguments);
for (int i = 0; i < mapped_count; ++i) {
int idx = Context::MIN_CONTEXT_SLOTS + parameter_count - 1 - i;
Node* value = graph()->NewNode(
......@@ -1521,7 +1530,8 @@ Node* JSCreateLowering::AllocateAliasedArguments(
graph()->NewNode(simplified()->NumberLessThan(), jsgraph()->Constant(i),
arguments_length),
jsgraph()->Constant(idx), jsgraph()->TheHoleConstant());
a.Store(AccessBuilder::ForFixedArraySlot(i + 2), value);
a.Store(AccessBuilder::ForFixedArrayElement(), jsgraph()->Constant(i + 2),
value);
}
return a.Finish();
}
......
......@@ -185,3 +185,56 @@
%OptimizeFunctionOnNextCall(f);
f(); f();
})();
// Test variable index access to strict arguments
// with up to 2 elements.
(function testArgumentsVariableIndexStrict() {
function g() {
"use strict";
var s = 0;
for (var i = 0; i < arguments.length; ++i) s += arguments[i];
return s;
}
function f(x, y) {
// (a) arguments[i] is dead code since arguments.length is 0.
const a = g();
// (b) arguments[i] always yields the first element.
const b = g(x);
// (c) arguments[i] can yield either x or y.
const c = g(x, y);
return a + b + c;
}
assertEquals(4, f(1, 2));
assertEquals(5, f(2, 1));
%OptimizeFunctionOnNextCall(f);
assertEquals(4, f(1, 2));
assertEquals(5, f(2, 1));
})();
// Test variable index access to sloppy arguments
// with up to 2 elements.
(function testArgumentsVariableIndexSloppy() {
function g() {
var s = 0;
for (var i = 0; i < arguments.length; ++i) s += arguments[i];
return s;
}
function f(x, y) {
// (a) arguments[i] is dead code since arguments.length is 0.
const a = g();
// (b) arguments[i] always yields the first element.
const b = g(x);
// (c) arguments[i] can yield either x or y.
const c = g(x, y);
return a + b + c;
}
assertEquals(4, f(1, 2));
assertEquals(5, f(2, 1));
%OptimizeFunctionOnNextCall(f);
assertEquals(4, f(1, 2));
assertEquals(5, f(2, 1));
})();
// Copyright 2018 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
// Test variable index access to array with 1 element.
(function testOneElementArrayVariableIndex() {
function f(i) {
const a = new Array("first");
return a[i];
}
assertEquals("first", f(0));
assertEquals("first", f(0));
%OptimizeFunctionOnNextCall(f);
assertEquals("first", f(0));
})();
// Test variable index access to array with 2 elements.
(function testTwoElementArrayVariableIndex() {
function f(i) {
const a = new Array("first", "second");
return a[i];
}
assertEquals("first", f(0));
assertEquals("second", f(1));
%OptimizeFunctionOnNextCall(f);
assertEquals("first", f(0));
assertEquals("second", f(1));
})();
// Copyright 2018 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
// Test variable index access to rest parameters
// with up to 2 elements.
(function testRestParametersVariableIndex() {
function g(...args) {
let s = 0;
for (let i = 0; i < args.length; ++i) s += args[i];
return s;
}
function f(x, y) {
// (a) args[i] is dead code since args.length is 0.
const a = g();
// (b) args[i] always yields the first element.
const b = g(x);
// (c) args[i] can yield either x or y.
const c = g(x, y);
return a + b + c;
}
assertEquals(4, f(1, 2));
assertEquals(5, f(2, 1));
%OptimizeFunctionOnNextCall(f);
assertEquals(4, f(1, 2));
assertEquals(5, f(2, 1));
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment