Bug 1740737 - Part 2: Handle arguments in optimised spread calls. r=iain

When the `arguments` object is still in its initial state (no overridden
elements, length, or iterator), `...arguments` can be optimised to directly
create the spread array from the arguments' content instead of going through
the iterator code.

The CacheIR code additionally disallows overridden or forwarded arguments in
order to further optimise this operation during scalar replacement. See part 4.

Differential Revision: https://phabricator.services.mozilla.com/D130987
This commit is contained in:
André Bargull 2021-11-18 11:37:29 +00:00
Родитель 1375b8c104
Коммит 109673303b
24 изменённых файлов: 846 добавлений и 14 удалений

Просмотреть файл

@ -0,0 +1,14 @@
function foo() {
return bar(arguments);
}
function bar(argsFromFoo) {
return baz(...argsFromFoo)
}
function baz(a,b) { return a + b; }
var sum = 0;
with ({}) {}
for (var i = 0; i < 100; i++) {
sum += foo(1,2);
}
assertEq(sum, 300);

Просмотреть файл

@ -0,0 +1,18 @@
function foo(x,y) {
function capture() { return x; }
return bar(arguments);
}
function bar(x) {
return baz(...x) + arguments.length;
}
function baz(x,y) {
return x + y;
}
var sum = 0;
for (var i = 0; i < 100; i++) {
sum += foo(1,2);
}
assertEq(sum, 400)

Просмотреть файл

@ -0,0 +1,17 @@
function foo(x,y) {
return bar(arguments);
}
function bar(x) {
return baz(...x) + arguments.length;
}
function baz(x,y) {
return x + y;
}
var sum = 0;
for (var i = 0; i < 100; i++) {
sum += foo(1,2);
}
assertEq(sum, 400)

Просмотреть файл

@ -0,0 +1,50 @@
// |jit-test| --fast-warmup
function foo(...args) {
with ({}) {}
return {result: args.length};
}
function inner() {
return arguments;
}
function outer0() {
trialInline();
var args = inner();
return new foo(...args);
}
function outer1() {
trialInline();
var args = inner(1);
return new foo(...args);
}
function outer2() {
trialInline();
var args = inner(1,2);
return new foo(...args);
}
function outer3() {
trialInline();
var args = inner(1,2,3);
return new foo(...args);
}
function outer4() {
trialInline();
var args = inner(1,2,3,4);
return new foo(...args);
}
with ({}) {}
for (var i = 0; i < 50; i++) {
assertEq(outer0().result, 0);
assertEq(outer1().result, 1);
assertEq(outer2().result, 2);
assertEq(outer3().result, 3);
assertEq(outer4().result, 4);
}

Просмотреть файл

@ -0,0 +1,50 @@
// |jit-test| --fast-warmup
function foo(...args) {
with ({}) {}
return args.length;
}
function inner() {
return arguments;
}
function outer0() {
trialInline();
var args = inner();
return foo(...args);
}
function outer1() {
trialInline();
var args = inner(1);
return foo(...args);
}
function outer2() {
trialInline();
var args = inner(1,2);
return foo(...args);
}
function outer3() {
trialInline();
var args = inner(1,2,3);
return foo(...args);
}
function outer4() {
trialInline();
var args = inner(1,2,3,4);
return foo(...args);
}
with ({}) {}
for (var i = 0; i < 50; i++) {
assertEq(outer0(), 0);
assertEq(outer1(), 1);
assertEq(outer2(), 2);
assertEq(outer3(), 3);
assertEq(outer4(), 4);
}

Просмотреть файл

@ -0,0 +1,39 @@
// |jit-test| --fast-warmup
function foo(a,b) {
with ({}) {}
return {result: a + b};
}
function inner(x,y) {
assertEq(x + y, new foo(...arguments).result);
}
function outer0() {
trialInline();
inner();
}
function outer1() {
trialInline();
inner(1);
}
function outer2() {
trialInline();
inner(1, 2);
}
function outer3() {
trialInline();
inner(1,2,3);
}
with ({}) {}
for (var i = 0; i < 50; i++) {
outer0();
outer1();
outer2();
outer3();
}

Просмотреть файл

@ -0,0 +1,47 @@
// |jit-test| --fast-warmup
function foo(...args) {
with ({}) {}
return {result: args.length};
}
function inner() {
// Single argument spread calls with |arguments| can be optimised.
return new foo(...arguments);
}
function outer0() {
trialInline();
return inner();
}
function outer1() {
trialInline();
return inner(1);
}
function outer2() {
trialInline();
return inner(1, 2);
}
function outer3() {
trialInline();
return inner(1,2,3)
}
function outer4() {
trialInline();
return inner(1,2,3,4)
}
with ({}) {}
for (var i = 0; i < 50; i++) {
assertEq(outer0().result, 0);
assertEq(outer1().result, 1);
assertEq(outer2().result, 2);
assertEq(outer3().result, 3);
assertEq(outer4().result, 4);
}

Просмотреть файл

@ -0,0 +1,39 @@
// |jit-test| --fast-warmup
function foo(a,b) {
with ({}) {}
return a + b;
}
function inner(x,y) {
assertEq(x + y, foo(...arguments));
}
function outer0() {
trialInline();
inner();
}
function outer1() {
trialInline();
inner(1);
}
function outer2() {
trialInline();
inner(1, 2);
}
function outer3() {
trialInline();
inner(1,2,3);
}
with ({}) {}
for (var i = 0; i < 50; i++) {
outer0();
outer1();
outer2();
outer3();
}

Просмотреть файл

@ -0,0 +1,47 @@
// |jit-test| --fast-warmup
function foo(...args) {
with ({}) {}
return args.length;
}
function inner() {
// Single argument spread calls with |arguments| can be optimised.
return foo(...arguments);
}
function outer0() {
trialInline();
return inner();
}
function outer1() {
trialInline();
return inner(1);
}
function outer2() {
trialInline();
return inner(1, 2);
}
function outer3() {
trialInline();
return inner(1,2,3)
}
function outer4() {
trialInline();
return inner(1,2,3,4)
}
with ({}) {}
for (var i = 0; i < 50; i++) {
assertEq(outer0(), 0);
assertEq(outer1(), 1);
assertEq(outer2(), 2);
assertEq(outer3(), 3);
assertEq(outer4(), 4);
}

Просмотреть файл

@ -0,0 +1,18 @@
function escape(x) { with ({}) {} }
function foo() {
escape(arguments);
return bar(...arguments);
}
function bar(x,y) {
return x + y;
}
with ({}) {}
var sum = 0;
for (var i = 0; i < 100; i++) {
sum += foo(1,2);
}
assertEq(sum, 300);

Просмотреть файл

@ -0,0 +1,16 @@
function foo() {
arguments[0] = 3;
return bar(...arguments);
}
function bar(x,y) {
return x + y;
}
with ({}) {}
var sum = 0;
for (var i = 0; i < 100; i++) {
sum += foo(1,2);
}
assertEq(sum, 500);

Просмотреть файл

@ -0,0 +1,20 @@
function escape() { with ({}) {} }
function foo(i) {
return i;
}
function bar(n) {
escape(arguments);
return foo(...arguments);
}
function baz(a, n) {
return bar(n);
}
var sum = 0;
for (var i = 0; i < 10000; i++) {
sum += baz(0, 1);
}
assertEq(sum, 10000);

Просмотреть файл

@ -0,0 +1,21 @@
var result;
function g(a, b) {
with ({}) {}
result = a + b;
}
function escape() { with({}) {} }
function f() {
escape(arguments);
for (var i = 0; i < 50; ++i) {
g(...arguments);
}
}
f(1, 2);
assertEq(result, 3);
f("");
assertEq(result, "undefined");

Просмотреть файл

@ -0,0 +1,216 @@
// Tests when |arguments| are used in optimized spread calls.
function testBasic() {
// Return the number of arguments.
function argslen() { return arguments.length; }
// Return the first argument.
function arg0() { return arguments[0]; }
// Return the argument at the request index.
function argIndex(i) { return arguments[i]; }
// Call the above functions when no formals are present.
function noFormalsLen() { return argslen(...arguments); }
function noFormals0() { return arg0(...arguments); }
function noFormalsIndex() { return argIndex(...arguments); }
// Call the above functions when some formals are present.
function formalsLen(x, y, z) { return argslen(...arguments); }
function formals0(x, y, z) { return arg0(...arguments); }
function formalsIndex(x, y, z) { return argIndex(...arguments); }
// Call the above functions when a rest argument is present.
function restLen(...rest) { return argslen(...arguments); }
function rest0(...rest) { return arg0(...arguments); }
function restIndex(...rest) { return argIndex(...arguments); }
// Call the above functions when default parameters are present.
function defaultLen(x = 0) { return argslen(...arguments); }
function default0(x = 0) { return arg0(...arguments); }
function defaultIndex(x = 0) { return argIndex(...arguments); }
for (let i = 0; i < 100; ++i) {
assertEq(noFormalsLen(), 0);
assertEq(noFormalsLen(1), 1);
assertEq(noFormalsLen(1, 2, 3), 3);
assertEq(formalsLen(), 0);
assertEq(formalsLen(1), 1);
assertEq(formalsLen(1, 2, 3), 3);
assertEq(restLen(), 0);
assertEq(restLen(1), 1);
assertEq(restLen(1, 2, 3), 3);
assertEq(defaultLen(), 0);
assertEq(defaultLen(1), 1);
assertEq(defaultLen(1, 2, 3), 3);
assertEq(noFormals0(), undefined);
assertEq(noFormals0(100), 100);
assertEq(noFormals0(100, 200, 300), 100);
assertEq(formals0(), undefined);
assertEq(formals0(100), 100);
assertEq(formals0(100, 200, 300), 100);
assertEq(rest0(), undefined);
assertEq(rest0(100), 100);
assertEq(rest0(100, 200, 300), 100);
assertEq(default0(), undefined);
assertEq(default0(100), 100);
assertEq(default0(100, 200, 300), 100);
assertEq(noFormalsIndex(), undefined);
assertEq(noFormalsIndex(0), 0);
assertEq(noFormalsIndex(0, 100), 0);
assertEq(noFormalsIndex(0, 100, 200, 300), 0);
assertEq(noFormalsIndex(1, 100), 100);
assertEq(noFormalsIndex(1, 100, 200, 300), 100);
assertEq(formalsIndex(), undefined);
assertEq(formalsIndex(0), 0);
assertEq(formalsIndex(0, 100), 0);
assertEq(formalsIndex(0, 100, 200, 300), 0);
assertEq(formalsIndex(1, 100), 100);
assertEq(formalsIndex(1, 100, 200, 300), 100);
assertEq(restIndex(), undefined);
assertEq(restIndex(0), 0);
assertEq(restIndex(0, 100), 0);
assertEq(restIndex(0, 100, 200, 300), 0);
assertEq(restIndex(1, 100), 100);
assertEq(restIndex(1, 100, 200, 300), 100);
assertEq(defaultIndex(), undefined);
assertEq(defaultIndex(0), 0);
assertEq(defaultIndex(0, 100), 0);
assertEq(defaultIndex(0, 100, 200, 300), 0);
assertEq(defaultIndex(1, 100), 100);
assertEq(defaultIndex(1, 100, 200, 300), 100);
}
}
//testBasic();
function testOverriddenIterator() {
function g(x) {
return x;
}
function f(i) {
if (i === 100) {
arguments[Symbol.iterator] = function() {
return ["bad"].values();
};
}
return g(...arguments);
}
for (let i = 0; i <= 150; ++i) {
assertEq(f(i), i !== 100 ? i : "bad");
}
}
testOverriddenIterator();
function testOverriddenLength() {
function g(x, y = 0) {
return x + y;
}
function f(i) {
if (i === 100) {
arguments.length = 1;
}
return g(...arguments);
}
for (let i = 0; i <= 150; ++i) {
assertEq(f(i, i), i !== 100 ? i * 2 : i);
}
}
testOverriddenLength();
function testOverriddenElement() {
function g(x, y) {
return x + y;
}
function f(i) {
if (i === 100) {
arguments[1] = 0;
}
return g(...arguments);
}
for (let i = 0; i <= 150; ++i) {
assertEq(f(i, i), i !== 100 ? i * 2 : i);
}
}
testOverriddenElement();
function testDeletedElement() {
function g(x, y = 0) {
return x + y;
}
function f(i) {
if (i === 100) {
delete arguments[1];
}
return g(...arguments);
}
for (let i = 0; i <= 150; ++i) {
assertEq(f(i, i), i !== 100 ? i * 2 : i);
}
}
testDeletedElement();
function testForwardedArg() {
function g(x, y) {
return x + y;
}
function f(i) {
function closedOver() {
if (i === 100) i = 0;
}
closedOver();
return g(...arguments);
}
for (let i = 0; i <= 150; ++i) {
assertEq(f(i, i), i !== 100 ? i * 2 : i);
}
}
testForwardedArg();
function testModifiedArrayIteratorPrototype() {
function g(x, y) {
return x + y;
}
const ArrayIteratorPrototype = Reflect.getPrototypeOf([][Symbol.iterator]());
const ArrayIteratorPrototypeNext = ArrayIteratorPrototype.next;
function newNext() {
var result = ArrayIteratorPrototypeNext.call(this);
if (!result.done) {
result.value *= 2;
}
return result;
}
function f(i) {
if (i === 100) {
ArrayIteratorPrototype.next = newNext;
}
return g(...arguments);
}
for (let i = 0; i <= 150; ++i) {
assertEq(f(i, i), i < 100 ? i * 2 : i * 4);
}
}
testModifiedArrayIteratorPrototype();

Просмотреть файл

@ -0,0 +1,15 @@
'use strict'
function bar(x,y) {
return x + y;
}
function foo(x, y) {
function closeOver() { return x; }
return bar(...arguments);
}
var sum = 0;
for (var i = 0; i < 100; i++) {
sum += foo(1,2);
}
assertEq(sum, 300)

Просмотреть файл

@ -0,0 +1,14 @@
function bar(x,y) {
return x + y;
}
function foo(x, y) {
function closeOver() { return x; }
return bar(...arguments);
}
var sum = 0;
for (var i = 0; i < 100; i++) {
sum += foo(1,2);
}
assertEq(sum, 300)

Просмотреть файл

@ -0,0 +1,18 @@
function foo() {
arguments.length = 2;
return bar(...arguments);
}
function bar() {
var result = 0;
for (var x of arguments) {
result += x;
}
return result;
}
var sum = 0;
for (var i = 0; i < 100; i++) {
sum += foo(1,2,3,4,5,6);
}
assertEq(sum,300);

Просмотреть файл

@ -4937,6 +4937,7 @@ AttachDecision OptimizeSpreadCallIRGenerator::tryAttachStub() {
AutoAssertNoPendingException aanpe(cx_);
TRY_ATTACH(tryAttachArray());
TRY_ATTACH(tryAttachArguments());
TRY_ATTACH(tryAttachNotOptimizable());
trackAttached(IRGenerator::NotAttached);
@ -5065,6 +5066,63 @@ AttachDecision OptimizeSpreadCallIRGenerator::tryAttachArray() {
return AttachDecision::Attach;
}
AttachDecision OptimizeSpreadCallIRGenerator::tryAttachArguments() {
// The value must be an arguments object.
if (!val_.isObject()) {
return AttachDecision::NoAction;
}
JSObject* obj = &val_.toObject();
if (!obj->is<ArgumentsObject>()) {
return AttachDecision::NoAction;
}
auto* args = &obj->as<ArgumentsObject>();
// Ensure neither elements, nor the length, nor the iterator has been
// overridden. Also ensure no args are forwarded to allow reading them
// directly from the frame.
if (args->hasOverriddenElement() || args->hasOverriddenLength() ||
args->hasOverriddenIterator() || args->anyArgIsForwarded()) {
return AttachDecision::NoAction;
}
NativeObject* arrayIteratorProto;
uint32_t slot;
JSFunction* nextFun;
if (!IsArrayIteratorPrototypeOptimizable(cx_, &arrayIteratorProto, &slot,
&nextFun)) {
return AttachDecision::NoAction;
}
ValOperandId valId(writer.setInputOperandId(0));
ObjOperandId objId = writer.guardToObject(valId);
if (args->is<MappedArgumentsObject>()) {
writer.guardClass(objId, GuardClassKind::MappedArguments);
} else {
MOZ_ASSERT(args->is<UnmappedArgumentsObject>());
writer.guardClass(objId, GuardClassKind::UnmappedArguments);
}
uint8_t flags = ArgumentsObject::ELEMENT_OVERRIDDEN_BIT |
ArgumentsObject::LENGTH_OVERRIDDEN_BIT |
ArgumentsObject::ITERATOR_OVERRIDDEN_BIT |
ArgumentsObject::FORWARDED_ARGUMENTS_BIT;
writer.guardArgumentsObjectFlags(objId, flags);
ObjOperandId protoId = writer.loadObject(arrayIteratorProto);
ObjOperandId nextId = writer.loadObject(nextFun);
writer.guardShape(protoId, arrayIteratorProto->shape());
// Ensure that proto[slot] == nextFun.
writer.guardDynamicSlotIsSpecificObject(protoId, nextId, slot);
writer.arrayFromArgumentsObjectResult(objId);
writer.returnFromIC();
trackAttached("Arguments");
return AttachDecision::Attach;
}
AttachDecision OptimizeSpreadCallIRGenerator::tryAttachNotOptimizable() {
ValOperandId valId(writer.setInputOperandId(0));

Просмотреть файл

@ -1619,6 +1619,7 @@ class MOZ_RAII OptimizeSpreadCallIRGenerator : public IRGenerator {
HandleValue val_;
AttachDecision tryAttachArray();
AttachDecision tryAttachArguments();
AttachDecision tryAttachNotOptimizable();
public:

Просмотреть файл

@ -41,6 +41,7 @@
#include "vm/FunctionFlags.h" // js::FunctionFlags
#include "vm/GeneratorObject.h"
#include "vm/GetterSetter.h"
#include "vm/Interpreter.h"
#include "vm/Uint8Clamped.h"
#include "builtin/Boolean-inl.h"
@ -8558,6 +8559,21 @@ bool CacheIRCompiler::emitMapGetObjectResult(ObjOperandId mapId,
return true;
}
bool CacheIRCompiler::emitArrayFromArgumentsObjectResult(ObjOperandId objId) {
JitSpew(JitSpew_Codegen, "%s", __FUNCTION__);
AutoCallVM callvm(masm, this, allocator);
Register obj = allocator.useRegister(masm, objId);
callvm.prepare();
masm.Push(obj);
using Fn = ArrayObject* (*)(JSContext*, Handle<ArgumentsObject*>);
callvm.call<Fn, js::ArrayFromArgumentsObject>();
return true;
}
bool CacheIRCompiler::emitBailout() {
JitSpew(JitSpew_Codegen, "%s", __FUNCTION__);

Просмотреть файл

@ -2780,6 +2780,13 @@
map: ObjId
obj: ObjId
- name: ArrayFromArgumentsObjectResult
shared: true
transpile: false
cost_estimate: 5
args:
obj: ObjId
- name: CallPrintString
shared: true
transpile: false

Просмотреть файл

@ -40,6 +40,7 @@ namespace jit {
js::ArgumentsObject::createForInlinedIon) \
_(ArgumentsObjectCreateForIon, js::ArgumentsObject::createForIon) \
_(ArrayConstructorOneArg, js::ArrayConstructorOneArg) \
_(ArrayFromArgumentsObject, js::ArrayFromArgumentsObject) \
_(ArrayJoin, js::jit::ArrayJoin) \
_(ArrayPushDense, js::jit::ArrayPushDense) \
_(ArraySliceDense, js::ArraySliceDense) \

Просмотреть файл

@ -66,6 +66,7 @@
#include "builtin/Boolean-inl.h"
#include "debugger/DebugAPI-inl.h"
#include "vm/ArgumentsObject-inl.h"
#include "vm/EnvironmentObject-inl.h"
#include "vm/GeckoProfiler-inl.h"
#include "vm/JSAtom-inl.h"
@ -4991,8 +4992,10 @@ bool js::SpreadCallOperation(JSContext* cx, HandleScript script, jsbytecode* pc,
return true;
}
bool js::OptimizeSpreadCall(JSContext* cx, HandleValue arg,
MutableHandleValue result) {
static bool OptimizeArraySpreadCall(JSContext* cx, HandleObject obj,
MutableHandleValue result) {
MOZ_ASSERT(result.isUndefined());
// Optimize spread call by skipping spread operation when following
// conditions are met:
// * the argument is an array
@ -5001,14 +5004,7 @@ bool js::OptimizeSpreadCall(JSContext* cx, HandleValue arg,
// * the array's prototype is Array.prototype
// * Array.prototype[@@iterator] is not modified
// * %ArrayIteratorPrototype%.next is not modified
if (!arg.isObject()) {
result.setUndefined();
return true;
}
RootedObject obj(cx, &arg.toObject());
if (!IsPackedArray(obj)) {
result.setUndefined();
return true;
}
@ -5021,15 +5017,106 @@ bool js::OptimizeSpreadCall(JSContext* cx, HandleValue arg,
if (!stubChain->tryOptimizeArray(cx, obj.as<ArrayObject>(), &optimized)) {
return false;
}
if (optimized) {
result.setObject(*obj);
} else {
result.setUndefined();
if (!optimized) {
return true;
}
result.setObject(*obj);
return true;
}
static bool OptimizeArgumentsSpreadCall(JSContext* cx, HandleObject obj,
MutableHandleValue result) {
MOZ_ASSERT(result.isUndefined());
// Optimize spread call by skipping the spread operation when the following
// conditions are met:
// * the argument is an arguments object
// * the arguments object has no deleted elements
// * arguments.length is not overridden
// * arguments[@@iterator] is not overridden
// * %ArrayIteratorPrototype%.next is not modified
if (!obj->is<ArgumentsObject>()) {
return true;
}
Handle<ArgumentsObject*> args = obj.as<ArgumentsObject>();
if (args->isAnyElementDeleted() || args->hasOverriddenLength() ||
args->hasOverriddenIterator()) {
return true;
}
ForOfPIC::Chain* stubChain = ForOfPIC::getOrCreate(cx);
if (!stubChain) {
return false;
}
bool optimized;
if (!stubChain->tryOptimizeArrayIteratorNext(cx, &optimized)) {
return false;
}
if (!optimized) {
return true;
}
auto* array = ArrayFromArgumentsObject(cx, args);
if (!array) {
return false;
}
result.setObject(*array);
return true;
}
bool js::OptimizeSpreadCall(JSContext* cx, HandleValue arg,
MutableHandleValue result) {
// This function returns |undefined| if the spread operation can't be
// optimized.
result.setUndefined();
if (!arg.isObject()) {
return true;
}
RootedObject obj(cx, &arg.toObject());
if (!OptimizeArraySpreadCall(cx, obj, result)) {
return false;
}
if (result.isObject()) {
return true;
}
if (!OptimizeArgumentsSpreadCall(cx, obj, result)) {
return false;
}
if (result.isObject()) {
return true;
}
MOZ_ASSERT(result.isUndefined());
return true;
}
ArrayObject* js::ArrayFromArgumentsObject(JSContext* cx,
Handle<ArgumentsObject*> args) {
MOZ_ASSERT(!args->hasOverriddenLength());
MOZ_ASSERT(!args->isAnyElementDeleted());
uint32_t length = args->initialLength();
auto* array = NewDenseFullyAllocatedArray(cx, length);
if (!array) {
return nullptr;
}
array->setDenseInitializedLength(length);
for (uint32_t index = 0; index < length; index++) {
const Value& v = args->element(index);
array->initDenseElement(index, v);
}
return array;
}
JSObject* js::NewObjectOperation(JSContext* cx, HandleScript script,
const jsbytecode* pc) {
if (JSOp(*pc) == JSOp::NewObject) {

Просмотреть файл

@ -616,6 +616,9 @@ bool SpreadCallOperation(JSContext* cx, HandleScript script, jsbytecode* pc,
bool OptimizeSpreadCall(JSContext* cx, HandleValue arg,
MutableHandleValue result);
ArrayObject* ArrayFromArgumentsObject(JSContext* cx,
Handle<ArgumentsObject*> args);
JSObject* NewObjectOperation(JSContext* cx, HandleScript script,
const jsbytecode* pc);