Core-js: [V8] Array.prototype.splice ~100x slower in V3 with symbol polyfill

Created on 24 Oct 2019  路  31Comments  路  Source: zloirock/core-js

This seems like it may be similar to #377. We recently discovered a perf issue after upgrading to v3.2.1 from v2 and tracked it down to a call to Array.prototype.splice that was occurring within a loop. Testing the performance of that method in isolation in environments with core-js/features/symbol applied revealed a similar issue.

I created a sample benchmark/repro that shows the relative performance with different versions.

compatibility help wanted v8

Most helpful comment

I added engine version detection and not calling this feature detection in modern V8. If feature detection or usage of Proxy will be required for any other case, I'll move it to the version, proposed by @schuay.

All 31 comments

core-js@3 add a polyfill of Array.prototype.splice with @@species support. Yes, it's seriously slower than native implementations. Because of the performance issue, it wasn't added in 2014-2015 when it was possible, but only in this year, when all modern engines support @@species natively and it's a performance problem mainly for old engines like IE11. Or you have this problem in modern engines?

Hmmm... Interesting example. Seems it's also an issue for modern V8. core-js does not replace Array.prototype.splice, it's still native, but we have performance degradation. I have no ideas.

Maybe one more kind of engine deoptimizations like #306? cc @schuay

@zloirock Thanks for the response. I see this behavior in Chrome 77 and Node 12.13.0. The issue seemed to be less significant in Firefox/Safari but the regression is still very much noticeable.

I found the problem place. It's calling of this test. We try to use Array.prototype.splice on an array with changed @@species. Seems V8 sees that we can do something like that and after that uses only a deoptimized version. Yes, it's an equal of #306.

It's V8 bug, not a core-js. I don't think that I can do something here because even removing this polyfill will be a breaking change. You could write it to V8 bug tracker.

A solution for you - avoid core-js/modules/es.array.splice module (maybe also could be required to disable other methods which use @@species on Array.prototype, but unlikely). You can do it with @babel/preset-env, useBuiltIns option and a blacklist.

cc @littledan

If I understood V8 source correctly, it's IsArraySpeciesProtectorCellInvalid, so it also affects Array#{ filter, map } :-(

Recent V8 versions also have a --trace-protector-invalidation flag that might help demonstrate what's going on:

out/debug/d8 --trace-protector-invalidation
d8> /./.constructor = () => {}
Invalidating protector cell RegExpSpeciesLookupChainProtector() => {}

I would still like to understand why core-js must touch these prototypes / species symbols. Above you said 'removing this polyfill will be a breaking change.' Could you explain (perhaps off-thread since this is slightly off-topic) what the polyfill does and why it must be applied even in recent V8 versions that should already implement the latest version of the spec?

Perhaps we can figure out a way to implement these polyfills without hitting all of V8's slow paths.

@schuay it's a polyfill for feature, which causes this deoptimization (in this case - @@species support in Array#splice). Without calling this feature (Array#splice on an object where @@species is not Array), we can't determinate is this feature supported or not.

Digging the V8 code, since Array#splice is generic, I found a possible workaround - calling Array#splice on a usual object with custom @@species instead of an array. (Really strange deoptimization 馃う.)

Digging the V8 code, since Array#splice is generic, I found a possible workaround - calling Array#splice on a usual object with custom @@species instead of an array. (Really strange deoptimization 馃う.)

Anything that avoids changing built-in prototypes (like Array.prototype.constructor) would certainly help.

Yes that certainly helps. We only care about constructor and @@species on relevant objects. See https://cs.chromium.org/chromium/src/v8/src/objects/lookup.cc?l=235&rcl=4f7ba9bb11d0641bd17655286f81106544790479.

E.g. for IsArraySpeciesLookupChainIntact:

  • constructor modifications on an array or the array prototype
  • @@species on the Array function.

@mathiasbynens in this case, we do not change built-in prototypes, we change .constructor property on an instance.

core-js is a polyfill - its job is to add or to change something on built-in prototypes :-D

@schuay but since RegExp methods are not generics - this approach will not work for them...

core-js is a polyfill - its job is to add or to change something on built-in prototypes :-D

The point is not to make these changes only when needed, i.e. not during feature detection.

@schuay we don't do it on feature detection.

@schuay I know, I wrote it above, I already wrote a workaround.

@schuay I know, I wrote it above, I already wrote a workaround.

Sounds great :+1:

@schuay but since RegExp methods are not generics - this approach will not work for them...

I'm not sure what you mean by generics, but RegExp methods do support subclassing. E.g. RegExp.prototype.test can be called on any object that has an exec method. https://tc39.es/ecma262/#sec-regexp.prototype.test

And e.g. @@split can be called on a non-RegExp object and considers @@species: https://tc39.es/ecma262/#sec-regexp.prototype-@@split

@schuay not all RegExp methods are generics, only a part of them (and, if I understood correctly, which not required for us) allows doing something like in the workaround above. I'll dig it later, maybe it's possible...

@schuay not all RegExp methods are generics, only a part of them (and, if I understood correctly, which not required for us) allows doing something like in the workaround above. I'll dig it later, maybe it's possible...

Feel free to ping me via email or on the tracker.

Seems I closed it too early. ArraySpeciesCreate creates instances of @@species only when it's called on an array, so it's not generic behavior. I didn't remember about this since I just awoke. The feature detection does not work, I should revert it. Any ideas of how we can detect it without deoptimization, @schuay @mathiasbynens?

Sure, we can just detect the engine version and do not call feature detection in modern V8:

if (V8_VERSION >= 51) return true;

but it is a very bad practice and used only in the absence of any alternatives.

Seems I closed it too early. ArraySpeciesCreate creates instances of @@species only when it's called on an array, so it's not generic behavior

Right, it only works if IsArray returns true, so for Arrays or Proxies on Arrays. This works, although I don't know if you can assume the existence of Proxies:

function f() { 
  const ctor = {}; 
  ctor[Symbol.species] = function () { 
    return { foo: 1 }; 
  }; 

  const a = [];
  const p = new Proxy(a, {
    get: function(obj, prop) {
      if (prop == "constructor") {
        return ctor;
      }
      return obj[prop];
    }
  });

  console.log(Array.prototype.splice.call(p, Boolean).foo);
}

f();

Spec text: https://tc39.es/ecma262/#sec-arrayspeciescreate

@schuay thanks, it's an interesting solution and it could work. However, 2 problems:

  • Proxy supported enough good and IIRC in all popular engines proper implementations were added before @@species on Array methods. However, we have old engines where Proxy had other APIs. Also, core-js should work everywhere - even in embed partial implementations where Proxy is missed. It will seriously complicate feature detection, but it's acceptable.
  • I鈥檓 afraid of another - I think that the Proxy itself could cause any deoptimization in any engines since the behavior of basic operations without the Proxy is much simpler than with.

What do you think?

Not sure I understand the second point, are you asking if the Proxy-based solution could somehow cause a global switch to flip to the slow path in other engines?

If so, anything is possible I guess.. But I think it's very unlikely given that no global state is modified in the proxy-based solution.

@schuay yep, something like that.

Let's think about alternative solutions for tomorrow - maybe we could find something better - and if not let's apply one of the mentioned above.

I added engine version detection and not calling this feature detection in modern V8. If feature detection or usage of Proxy will be required for any other case, I'll move it to the version, proposed by @schuay.

Thanks, benchmark results confirm that this worked!

Thanks! Are you planning to do something similar for other builtins, especially RegExp?

Edit: Just saw https://github.com/zloirock/core-js/issues/679.

I experienced the same issue with v3 in Internet Explorer 11, but not in other browsers, so we had to rollback to v2 because IE 11 was completely frozen.

Is it a known issue or has anyone else experienced the same?
Appreciate any help on this, since we are currently stuck on the legacy core-js v2 and can't upgrade because we have to support IE 11.

@slowcheetah could you provide some idea of what's going on with this issue? I'm confused about whether or not this is fixed.

I experienced the same issue with v3 in Internet Explorer 11, but not in other browsers, so we had to rollback to v2 because IE 11 was completely frozen. Is it a known issue or has anyone else experienced the same?
Appreciate any help on this, since we are currently stuck on the legacy core-js v2 and can't upgrade because we have to support IE 11.

It's definitely not this error. Without any additional information, I can't say anything.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

fender picture fender  路  5Comments

koenpunt picture koenpunt  路  5Comments

mryellow picture mryellow  路  6Comments

blondie63 picture blondie63  路  4Comments

ajbowler picture ajbowler  路  3Comments