Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement Array.prototype.filter in javascript #5137

Merged
merged 1 commit into from
May 15, 2018

Conversation

sigatrev
Copy link
Contributor

@sigatrev sigatrev commented May 11, 2018

Implementing built-ins in JavaScript allows the JIT to optimize calls that must otherwise behave generically. This can provide significant performance improvements, particularly for array operations that loop over the array.

In a microbenchmark calling filter 1000 times on a 10000 element array, the JS implementation was 13x faster in the best case, and 20% faster in the worst case.

for (var i = 0; i < 1000; ++i) {
  a.filter(()=>false) // best case, array is never grown
}
// js: 30 ms
// c++: 400ms

for (var i = 0; i < 1000; ++i) {
  a.filter(()=>true) // worst case
}
// js: 550 ms
// c++: 700 ms

Adding a JIT fast path for __chakraLibrary.arrayCreateDataPropertyOrThrow similar to normal array assignment would drop the always-true case to 230 ms.

Copy link
Contributor

@zenparsing zenparsing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS implementation looks good.


let kValue = o[k];
if (boundCallback(kValue, k, o))
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: curly should probably go on previous line.

Copy link
Contributor

@meg-gupta meg-gupta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Would be good to run test262.


let o;
let len
if (__chakraLibrary.isArray(this)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not do Array.isArray()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls the same function, but from a safe location so that we are unaffected if the user replaces Array.isArray

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah - thanks,


In reply to: 187739035 [](ancestors = 187739035)

Var JsBuiltInEngineInterfaceExtensionObject::EntryJsBuiltIn_Internal_ArraySpeciesCreate(RecyclableObject *function, CallInfo callInfo, ...)
{
EngineInterfaceObject_CommonFunctionProlog(function, callInfo);
Assert(args.Info.Count == 3);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assert [](start = 8, length = 6)

AssertOfFailFast

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose a normal Assert because the function is callable only through the JS built-ins and not in any user controlled way. If you think it needs to be a FailFast I'll change it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be better be failfast it is just a check.


In reply to: 187739964 [](ancestors = 187739964)


if (newObj == nullptr)
{
newArr = scriptContext->GetLibrary()->CreateArray(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 [](start = 62, length = 1)

should this be args.Values[2]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it should be. thanks

} else {
o = __chakraLibrary.Object(this);
len = __chakraLibrary.GetLenth(o);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetLenth [](start = 34, length = 8)

+g

let kValue = o[k];
if (boundCallback(kValue, k, o))
{
a[to] = kValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is CreateDataPropertyOrThrow? will it perform the same as the spec wants it to?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this will, but @zenparsing can hopefully confirm that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akroshg Good call, it's not quite the same. Assignment will utilize the prototype chain, whereas filter creates a data property directly on the output array.

Object.defineProperty(Array.prototype, '0', {
  get() { return 980 },
  set(value) { console.log('called setter') },
});

// Does not log "called setter"
[1].filter(() => true);

// Logs "called setter"
let a = [];
a[0] = 1;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best course of action here is to use the CreateDataPropertyOrThrow in the case when species returns anything other than an Array.


In reply to: 187750568 [](ancestors = 187750568)

let boundCallback = callbackfn;
if (thisArg === undefined) {
boundCallback = callbackfn;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove the if part, as you have initialized boundCallback already?

assert.throws(function () { Array.prototype.map.call(arr, function (a) { return a;}); }, TypeError, desc + "map", error);
assert.throws(function () { Array.prototype.filter.call(arr, function (a) { return true;}); }, TypeError,desc + "filter", error);
assert.throws(function () { Array.prototype.filter.call(arr, function (a) { return true;}); }, TypeError,desc + "filter", errorBuiltIn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't change, no? This may be happening due to CreateDataPropertyOrThrow in not there?

@@ -453,5 +460,34 @@ namespace Js
return obj;
}

Var JsBuiltInEngineInterfaceExtensionObject::EntryJsBuiltIn_Internal_ArraySpeciesCreate(RecyclableObject *function, CallInfo callInfo, ...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

future todo : I think ArraySpeciesCreate should be implemented entirely in javascript. May be in future releases

@akroshg
Copy link
Contributor

akroshg commented May 11, 2018

I believe the description should stats what is the motivation behind the change. Please share improvements on the micro-benchmark (crafted filter scenario should be fine).

}
}

return newObj;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be returning newArr? if not, whats the point of newArr?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is essentially copied from JavascriptArray::FilterHelper. I don't have a good understanding of under what circumstances ArraySpeciesCreate would return a JavascriptCopyOnAccessNativeIntArray that needs conversion so I left that part of the logic the same. On closer inspection though, yeah, newArr is redundant and unnecessary.

@@ -35,6 +36,9 @@
__chakraLibrary.ArrayIterator.prototype = CreateObject(iteratorPrototype);
__chakraLibrary.raiseNeedObjectOfType = platform.raiseNeedObjectOfType;
__chakraLibrary.raiseThis_NullOrUndefined = platform.raiseThis_NullOrUndefined;
__chakraLibrary.raiseFunctionArgument_NeedFunction = platform.raiseFunctionArgument_NeedFunction;
__chakraLibrary.callInstanceFunc = platform.builtInCallInstanceFunction;
__chakraLibrary.functionBind = platform.builtInJavascriptFunctionEntryBind;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly is the goal here? Why does __chakraLibrary need these if you already have access to them through platform

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't allow access to any functions that are not on __chakraLibrary from within the built in functions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not? Intl definitely doesn't have this restriction since it doesn't use __chakraLibrary at all.

let to = 0;

// Binding: If thisArg is provided, we need to do the equivalent of callbackfn.call(thisArg, ...), but we cannot use .call because Function.prototype.call can be overriden.
// Unfortinately, locally recording the original Function.protoype.call would need to call originalCall.call(callbackfn, thisArg, ...), which has the same .call problem.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortinately [](start = 11, length = 13)

nit: spelling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

REM pushd %_reporoot%\lib\Runtime\Library\InJavascript
REM call GenByteCode.cmd
REM call GenByteCode.cmd -nojit
REM popd
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops?

if (k in o) {
let kValue = o[k];
if (callbackfn(kValue, k, o)) {
a[to] = kValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you not still have an array that passes Array.isArray but has a non-writable property?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you are correct, at the very least through modifications of Array.prototype.

@rhuanjl
Copy link
Collaborator

rhuanjl commented May 12, 2018

Thought on the slow path is it functionally equivalent to the fast path when in strict mode? Could there be a strict mode only optimisation to always use the fast path? Possibly blocked by: #5046

if (k in o) {
let kValue = o[k];
if (callbackfn(kValue, k, o)) {
a[to] = kValue;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will still have the problem as Kevin mentioned before - it will call setter when we execute this, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm about to update the PR with a version that always calls the __chakraLibrary helper, but I'm running test262 and perf results for it first

@sigatrev sigatrev force-pushed the jsFilter branch 4 times, most recently from d849073 to 1ff7409 Compare May 15, 2018 01:58
@chakrabot chakrabot merged commit c648792 into chakra-core:master May 15, 2018
chakrabot pushed a commit that referenced this pull request May 15, 2018
Merge pull request #5137 from sigatrev:jsFilter

Implementing built-ins in JavaScript allows the JIT to optimize calls that must otherwise behave generically. This can provide significant performance improvements, particularly for array operations that loop over the array.

In a microbenchmark calling filter 1000 times on a 10000 element array, the JS implementation was 13x faster in the best case, and 20% faster in the worst case.

```
for (var i = 0; i < 1000; ++i) {
  a.filter(()=>false) // best case, array is never grown
}
// js: 30 ms
// c++: 400ms

for (var i = 0; i < 1000; ++i) {
  a.filter(()=>true) // worst case
}
// js: 550 ms
// c++: 700 ms
```

Adding a JIT fast path for __chakraLibrary.arrayCreateDataPropertyOrThrow similar to normal array assignment would drop the always-true case to 230 ms.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants