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

Turn off negate_iife by default as it hurts V8 performance. #886

Closed
krisselden opened this issue Dec 10, 2015 · 28 comments
Closed

Turn off negate_iife by default as it hurts V8 performance. #886

krisselden opened this issue Dec 10, 2015 · 28 comments

Comments

@krisselden
Copy link

Removing the ( before iife causes V8 to preparse to skip function bodies in top level iifes (when the script is large) then parse it when executing and also affects the hinting for lazy function body compiling.

From the V8 preparser.h:

        // Heuristically try to detect immediately called functions before
        // seeing the call parentheses.
        parenthesized_function_ = (peek() == Token::FUNCTION);
@rvanvelzen
Copy link
Collaborator

Do you have some actual numbers showing the difference in performance?

@mishoo
Copy link
Owner

mishoo commented Dec 10, 2015

I second @rvanvelzen. Can't believe there is any significant difference, or else I'll make fun of @mraleph. >-)

@mraleph
Copy link

mraleph commented Dec 10, 2015

@mishoo I haven't touched V8 source for 3 years now - no need to make fun of me ;)

@krisselden
Copy link
Author

@mishoo this is on an internal app and I'm focused on android performance issues testing on an S3, I'll pull together the numbers for you but if I do, please don't make fun of anyone or tweet it.

@mraleph
Copy link

mraleph commented Dec 10, 2015

@krisselden you should report the numbers to V8 folks. I am sure they'll be interested. Startup issues caused by overly zealous lazy parsing heuristics have been increasingly on the radar lately AFAIK.

@krisselden
Copy link
Author

@mraleph in a single page app, they help by deferring components and templates you don't use in initial render is actually pretty nice, it is just a bit touchy because it is double parsing a lot of code that is needed for initial render.

@mishoo
Copy link
Owner

mishoo commented Dec 10, 2015

Just kidding. @mraleph is one of the greatest compiler/JIT engineers and V8 rocks.

Any case, can't believe this small thing could cause a big slowdown — if it does then the V8 folks definitely want to know about it.

@krisselden
Copy link
Author

It is hard to reduce this to a simple example because the issue is deferring work that happens when you use code in the app, just reducing it to a simple example really alters the trade off, I also regret because I think changing the default is a bit alarmist and I was tired and frustrated. It is good enough for me that it is optional.

@jdalton
Copy link

jdalton commented Jul 10, 2016

Related to #543, #640, and rollup/rollup#774.

@nolanlawson
Copy link

FWIW I made a benchmark around this, seems to confirm it's a perf boost for V8 and Chakra: rollup/rollup#774 (comment)

@krisselden
Copy link
Author

they should add this style to there iife detection

I'm also concerned that as we bring attention to this lots of people are going to start wrapping parens around everything forcing the browser to compile stuff that should be lazy.

V8 also recently has improved parse perf

@nolanlawson
Copy link

Yeah the tricky part here is that it's not the case that you want to add parens around functions willy-nilly, but that you want to do it everywhere where you know the function will be executed.

The current optimization in Chakra (and I assume for V8 as well) is due to the fact that most JS functions out in the wild are never executed (i.e. web authors ship a lot of unused code), so it's a perf boost to lazy-parse. However when people ship highly-optimized tree-shaken bundles where every function gets immediately executed, it's a perf regression to do the lazy-parse.

Probably the main problem with the current negate_iife default in Uglify is that it prevents bundlers from even implementing such optimizations. Even if they know exactly which modules will be immediately executed, any parens they add will just be dropped. Yes developers can change the default, but in practice nobody does this.

Also worth pointing out: this optimization doesn't just apply to IIFEs - it also applies to any paren-wrapped functions that are immediately executed. So it's not clear that changing the default negate_iife behavior would fully fix this... 😕

@nolanlawson
Copy link

nolanlawson commented Sep 16, 2016

OK, so I ran some more benchmarks and have some updated info on this.

When Uglify converts (function(){})()-style IIFEs into !function(){}()-style IIFEs, it's actually not a perf regression in either Chrome or Edge. Yay!

So that just leaves functions enclosed in parens in other situations, such as the original benchmark where we took a UMD bundle (essentially a function passed in to another function, and then executed). In those cases, it's still a perf regression in both Chrome and Edge to remove the parens.

Since this case has nothing to do with negate_iife (i.e. Uglify will remove the parens either way), this particular issue should probably remain closed.

Edit: my bad, I was wrong, see below.

@krisselden
Copy link
Author

@nolanlawson your benchmarks are bad, at least for Chrome, the heuristics in the top level of a large script (above 1024) follow the preparse rules which are more simplistic.

Also, this option seems to affect unrelated things: https://gist.github.com/krisselden/c43d47be74dcc3e870a8c014c6e68079

@krisselden
Copy link
Author

Just to clarify, the preparse phase is only for script files and only top level function literals. Lazy parsing still happens for literals below but the heuristic isn't a simple.

@nolanlawson
Copy link

@krisselden I'm sorry, I don't understand what you mean; can you clarify? Also my benchmark is open-source, so please feel free to fork to show me what you mean. 😃

Based on that benchmark it seems to me that unwrapped UMD and declare-then-execute are the only two unoptimized ones in either Chrome or Edge, but I may have made a mistake.

@krisselden
Copy link
Author

krisselden commented Sep 16, 2016

After tracing your benchmark, the only reason they are the same is because you wrapped them in a function literal on the top level that went through the preparse phase.

image

image

Both lazy parsed the function immutable() {

@krisselden
Copy link
Author

having trouble with github's image upload, but you can't wrap your script in a function literal, this is a top level thing with chrome.

@nolanlawson
Copy link

Hm OK in that case I need to go back and rewrite the bench without the top-level immutable function. Thanks for the tip, and sorry for derailing the conversation!

@krisselden
Copy link
Author

@nolanlawson I've trolled myself many times with benchmarks, it is important to check your work, but at least you made it verifiable and public.

@nolanlawson
Copy link

Thanks for calling me out @krisselden; here's a corrected benchmark. My results show that Edge is optimized for the !function(){}() style, but Chrome and Firefox are not. Chrome 52 and Firefox 48 seem to optimize for both Crockford and regular-style IIFEs, as well as the wrapped UMD style, but not the unwrapped UMD or declare-then-exec. In all other ways, Edge is the same, except that it also optimizes for the Uglify style. As for Safari, I'm not sure.

@kzc
Copy link
Contributor

kzc commented Sep 16, 2016

On Safari it errors out with:

ReferenceError: Can't find variable: performance
uglify-style.js:1

@kzc
Copy link
Contributor

kzc commented Sep 16, 2016

@nolanlawson I'm seeing different results for Chrome than what you describe.

Chrome on Mac Version 53.0.2785.116 (64-bit)

Run unwrapped UMD: 7.849999999998545ms
Run wrapped UMD: 7.849999999998545ms
Run function func () {}; func() version: 7.670000000001892ms
Run Uglify-style !function () {}() version: 7.849999999998545ms
Run Crockford-style (function () {}()) version: 1.1200000000026193ms
Run regular IIFE version - (function () {})(): 1.1200000000026193ms

Edit: My bad. It's consistent with your observations.

@kzc
Copy link
Contributor

kzc commented Sep 16, 2016

For Firefox 48.0.1 on Mac I'm seeing 2.8ms across all techniques.

@nolanlawson
Copy link

nolanlawson commented Sep 16, 2016

You may have to switch to a slower computer to see the difference in Firefox. I tested 48.0 in both Mac and Windows and I see the same perf characteristics as Chrome. Here's a typical run:

Unwrapped UMD: 3.894999999999982ms
Wrapped UMD: 1.4099999999998545ms
function func () {}; func(): 3.8299999999999272ms
Uglify-style: 3.7849999999998545ms
Crockford-style: 1.5750000000007276ms
Regular IIFE: 2.1700000000018917ms

So the slow version is ~3.5-4ms and the fast version is ~1.5-2ms. Perhaps it would be easier to discern if I used a larger library than Immutable.js.

I tested in Safari 10 and can't repro your performance problem. Are you on Safari 8+?

@kzc
Copy link
Contributor

kzc commented Sep 16, 2016

Safari 9.1.3. Is 10 out of beta?

@nolanlawson
Copy link

No, I'm on macOS Sierra, but it's still a preview.

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

No branches or pull requests

7 participants