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

Optimise __extends #9925

Closed
Rycochet opened this issue Jul 25, 2016 · 15 comments
Closed

Optimise __extends #9925

Rycochet opened this issue Jul 25, 2016 · 15 comments
Labels
Needs More Info The issue still hasn't been fully clarified Suggestion An idea for TypeScript

Comments

@Rycochet
Copy link

Rycochet commented Jul 25, 2016

I was trying to find places to optimise a large project, and realised that the longest running function was actually the __extends call, which prompted me to look at the current code.

Current code

Two things immediately jumped out at me with the current code:

var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};

Problems

Potentially unused code (pre ES5)

The first problem was that the __() function was being created, when it might not be used (though chances of that are probably very small).

The Solution would be to move the check into an if/else. Related to this is that null doesn't have any properties, so the property copying should be in the "not null" section.

var __extends = (this && this.__extends) || function (d, b) {
    if (b===null) {
        d.prototype = Object.create(b);
    } else {
        for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
        var __=function() { this.constructor = d; };
        __.prototype = b.prototype;
        d.prototype = new __();
    }
};

Inefficient iteration (for ES5+)

The second problem is purely performance related with the property copying. for(var p in b) will iterate over the entire prototype tree, but if(b.hasOwnProperty(p)) will then skip any that aren't directly on the object/class itself. (see Enumerability and ownership of properties)

The solution would be to use Object.keys(b) and iterating an array, which will work only on the object/class itself (and not it's prototype tree), it also automatically skips the same keys we'd be skipping anyway - so taking most of what we're wanting to the browser / native code instead of JS.

Unlike for...in, Object.keys fails when called on null, undefined or any other non-Object, so this requires some extra code to help prevent that happening.

From here we see that Object.keys() is an ES5+ function.

Suggested code:

var __extends = (this && this.__extends) || function (d, b) {
    if (b instanceof Object) {
        Object.keys(b).forEach(function(p){
            d[p] = b[p];
        });
        var __=function() { this.constructor = d; };
        __.prototype = b.prototype;
        d.prototype = new __();
    } else {
        d.prototype = Object.create(null);
    }
};

Results

It's very hard to get reliable timings for the code on Chrome Profiling, but on average this looks like roughly a 10% speedup at startup for my project (38 classes in a nested tree up to six deep). There is no major benefit to this, though it does make the code slightly "safer".

The ES5+ code could potentially cause different behaviour if a class tries to inherit from a non-class (such as a number or string), though I'm not sure that is possible to begin with..

Language Feature Checklist

  • Syntactic: No changes as this is a compiler change.
  • Semantic: No changes as the final code does the same thing when used as intended (see Results above).
  • Emit: Output code will change depending on ES version needed, both versions are slightly larger than the original.
  • Compatibility: Two versions, one for pre-ES5, most efficient for ES5+
  • Other: There is already an option to prevent __extends being output (which only happens if needed anyway). This could potentially hit older build scripts that manually search & replace the old code.
@yortus
Copy link
Contributor

yortus commented Jul 25, 2016

I'm pretty sure moving the __() function into an if/else would make an unmeasurable difference. The conditional check there is just to cover the extraordinarily special case that someone writes a class like class Foo extends null {...} which is technically valid so must be supported. That's the only time the __() function is wastefully created, but it must be like 0.001% of cases (I doubt you'd have any such cases in the code you're measuring, and personally I've never encountered or found a use for extends null).

So if you are registering a speed improvement, I'd guess it's all from the for..in changes you made.

@Rycochet
Copy link
Author

It is - the if/else thing is as much because I'm also doing ePub3 work, where you try to be a bit more efficient with the garbage collector - but it just makes logical sense to not create something that's never going to get used ;-)

@yortus
Copy link
Contributor

yortus commented Jul 25, 2016

This part also seems odd...

[in] a large project [...] the longest running function was actually the __extends call

Given that __extends is called only once per class declaration (and not every time a class instance is created), your large project must be spending a huge proportion of it's time creating new classes (not just instantiating them).

I can only imagine this would be via class expressions that are repeatedly executed on some hot path. If there was some way to avoid continual re-evaluation of class expressions (eg by moving them out to a normal module-level class declaration, perhaps with some extra constructor parameters), I imagine __extends would no longer be a hot function, and you'd probably get an even bigger speedup.

@Rycochet
Copy link
Author

The cause of me looking at it isn't really relevant - was just the reporting method I was using and it grouping things together - hence why I saw the two minor issues in it (it took longer writing this issue out that changing and testing the code lol) - it's also why it's really hard to measure the speed difference - for something that's called only once per class it's the iterating that takes the longest time as nothing else is that expensive - and even that expense depends on how much is actually inside the class and tree (mine is pretty big, hence the 10%).

Saving 50ms is worth it for me, not sure it'd make any difference for 99% or more of the projects using TS, but better code is always worth talking about ;-)

@yortus
Copy link
Contributor

yortus commented Jul 25, 2016

it just makes logical sense to not create something that's never going to get used ;-)

Bear in mind that function declarations are hoisted, so just because you lexically nest it inside an if statement, doesn't necessarily mean the function isn't created anyway. I think the behaviour is not well-defined across all js engines. It's definitely an error in strict mode to nest a function declaration inside an if block. So it's probably best to avoid.

@Rycochet
Copy link
Author

var __=function() { this.constructor = d; };

@yortus
Copy link
Contributor

yortus commented Jul 25, 2016

Got me there :)

@Rycochet
Copy link
Author

You were completely right though, hence I changed the issue code :-P

@yortus
Copy link
Contributor

yortus commented Jul 25, 2016

Just a hypothetical....

If you had a project that creates 1,000,000 instances across 10 different classes, then __extends would be called only 10 times and the GC pressure of those 10 __() instances (say 9 of which are necessary anyway) would be utterly negligable, and even the tiniest tweak to the code that repeats 1 million times would yield far greater gains.

OTOH, if your project has __extends on a hot path, then its more like creating 1 instance each of 1,000,000 class expressions. That just seems a wildly inefficient approach to using classes, and again the gains would be far greater in factoring out the class expressions to reduce the number of __extends calls by several orders of magnitude.

@RyanCavanaugh
Copy link
Member

Seconding @yortus 's comments that perf of __extends should not be measurable in any reasonable scenario. If this is your primary objective (rather than improved semantic correctness), the best thing to do would be to have your own __extends at global scope before the first TS code runs so that that _-extends takes precedence. Alternatively, you could run with --noEmitHelpers and write an explicit __extends somewhere in your own codebase.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Needs More Info The issue still hasn't been fully clarified labels Jul 25, 2016
@Rycochet
Copy link
Author

The primary goal of this code is to be more "correct", the speedup is a side effect of that - the final in-memory classes used will be identical so it's purely a case of changing how it gets there slightly. I'd hate to see any code with a million classes in it!

@RyanCavanaugh
Copy link
Member

One problem here is that we need the __extends code to work on ES3, so we can't use Object.create.

You might say "Easy fix, you can emit different __extends based on the --target" but this is a cure worse than the disease -- because __extends is found in the global scope, you could get into a state where a "more compatible" ES3-compiled TypeScript library loaded before an ES5 TypeScript user codebase would change the behavior of the user code! Or vice versa, if someone was somehow intentionally depending on the non-spec behavior of __extends.

@Rycochet
Copy link
Author

Good point - though it would be a problem if an ES5 class was loaded before an ES3 one I think - but do ES6+ classes even get this included?

Having a slightly longer version that checks would fix it - but not sure that the extra code would be worth it:

var __extends = (this && this.__extends) || function (d, b) {
    if (b instanceof Object) {
        if (Object.keys) {
            Object.keys(b).forEach(function(p){
                d[p] = b[p];
            });
        } else {
            for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
        }
        var __=function() { this.constructor = d; };
        __.prototype = b.prototype;
        d.prototype = new __();
    } else {
        d.prototype = Object.create(null);
    }
};

@nin-jin
Copy link

nin-jin commented Jul 31, 2016

interface Object {
     // Type system in typescript is too weak.
    assign( target : any , ...sources : any[] ) : any
}

/// Inheritence with static constructor support
var __extends = ( Sub , Sup ) => {

    /// Inherit static properties
    Object.assign( Sub , Sup )

    /// Create inherited prototype without parent constructor calling
    Sub.prototype = Object.create( Sup.prototype , {
        constructor : {
            configurable : true ,
            writable : true ,
            value : Sub ,
        }
    } )

    /// Call static constructor
    if( Sub.initializer ) Sub.initializer()
}

@nin-jin
Copy link

nin-jin commented Jul 31, 2016

You can boldly include polyfills for Object.assign and Object.create because it is standard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs More Info The issue still hasn't been fully clarified Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants