Unlike the spec we chose BigInt for the input and output types here as
it was being used with ℝ(ns), ns being of type BigInt, in one place and
a conversion to double would not be safe.
Since in many places we'll have double input values, let's add a double
overload of this function to avoid awkward conversions and expensive
allocations.
This patch introduces the "environment coordinate" concept, which
encodes the distance from a variable access to the binding it ends up
resolving to.
EnvironmentCoordinate has two fields:
- hops: The number of hops up the lexical environment chain we have
to make before getting to the resolved binding.
- index: The index of the resolved binding within its declarative
environment record.
Whenever a variable lookup resolves somewhere inside a declarative
environment, we now cache the coordinates and reuse them in subsequent
lookups. This is achieved via a coordinate cache in JS::Identifier.
Note that non-strict direct eval() breaks this optimization and so it
will not be performed if the resolved environment has been permanently
screwed by eval().
This makes variable access *significantly* faster. :^)
Since non-strict direct eval() can insert new bindings into a
surrounding var scope, we cannot safely cache some assumptions about
environment chain layout after eval() has taken place.
Since eval() is rare, let's do what other engines do and simply
deoptimize in its presence. This patch adds a new "permanently screwed"
flag to JS::Environment that will be set on the entire variable
environment chain upon non-strict direct eval().
VM::resolve_binding() can now return a Reference that knows the exact
binding index if it's pointing into a DeclarativeEnvironment.
Reading/writing through the Reference will now use direct environment
access when possible.
This patch adds two DeclarativeEnvironment APIs:
- get_binding_value_direct()
- set_mutable_binding_direct()
These work identically to their non-direct-suffixed counterparts, but
take an index instead of a bound name. This will allow someone who has
a binding index to get/set that binding directly without any additional
hash lookups.
The previous storage for DeclarativeEnvironment looked like this:
HashMap<FlyString, Binding> m_bindings;
This patch changes that to:
HashMap<FlyString, size_t> m_names;
Vector<Binding> m_bindings;
The main goal here is to give each binding an index that can ultimately
be cached and used for optimized environment accesses.
Similarly to regexp_initialize() this can be a member function instead
of taking a RegExpObject argument.
Having it available outside RegExpPrototype is also useful for other
things that need RegExp.prototype.source behavior - e.g. the REPL for
pretty-printing.
Instead of iterating *all* swept cells when pruning weak containers,
only iterate the cells actually *in* the container.
Also, instead of compiling a list of all swept cells, we can simply
check the Cell::state() flag to know if something should be pruned.
Instead of checking storage_has(), followed by storage_get(), we can do
storage_get() directly and avoid a redundant property lookup.
This exposed a bug in SimpleIndexedPropertyStorage::get() which would
previously succeed for array holes.
This gives FunctionNode a "might need arguments object" boolean flag and
sets it based on the simplest possible heuristic for this: if we
encounter an identifier called "arguments" or "eval" up to the next
(nested) function declaration or expression, we won't need an arguments
object. Otherwise, we *might* need one - the final decision is made in
the FunctionDeclarationInstantiation AO.
Now, this is obviously not perfect. Even if you avoid eval, something
like `foo.arguments` will still trigger a false positive - but it's a
start and already massively cuts down on needlessly allocated objects,
especially in real-world code that is often minified, and so a full
"arguments" identifier will be an actual arguments object more often
than not.
To illustrate the actual impact of this change, here's the number of
allocated arguments objects during a full test-js run:
Before:
- Unmapped arguments objects: 78765
- Mapped arguments objects: 2455
After:
- Unmapped arguments objects: 18
- Mapped arguments objects: 37
This results in a ~5% speedup of test-js on my Linux host machine, and
about 3.5% on i686 Serenity in QEMU (warm runs, average of 5).
The following microbenchmark (calling an empty function 1M times) runs
25% faster on Linux and 45% on Serenity:
function foo() {}
for (var i = 0; i < 1_000_000; ++i)
foo();
test262 reports no changes in either direction, apart from a speedup :^)
In ECMAScriptFunctionObject::function_declaration_instantiation() we
iterate over all lexically declared names of the function scope body to
determine whether any of them is named 'arguments', because we don't
need to create an arguments object in that case. We can also stop at
that point, because the decision won't change anymore.