Faster JavaScript Calls
v8.dev
v8.dev
If you're already using TypeScript to constrain your code, why couldn't it generate interpreter/compiler hints?
These could take the form of a language-agnostic metadata file, similar to source maps, that optionally ships alongside the JS bundle
Edit: Several people have misunderstood, so I must not have articulated this well. What I meant was not a new/stricter subset of JS that forbids certain dynamic behaviors across the board. What I'm talking about is having TypeScript (or otherwise), which knows things at compile-time about how specific pieces of code are and are not called, manifest this information in a way that V8 can digest and act on directly, for that specific codebase. V8 already tries to guess this information and uses it to decide which things to optimize and how, but it's treating the JS bundle as a black box even though in many cases, an earlier stage of the pipeline already had this information on-hand and threw it away.
In addition to being more granular, this (like TypeScript itself) would allow parts of a codebase to continue being fully dynamic while other parts are well-constrained.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
"Function X will only ever take two arguments, they will be of types Y and Z"
"This object will only ever have these properties; it will never have properties created or deleted"
"This prototype will never be modified at runtime"
The only real way to make that work would be a new `<script type="typed-module" />` variant that implemented a subset of JS in a typed environment.
My guess is that this could happen in the future, but everyone is watching Typescript as a testing ground to find exactly what works, what doesn't, and what works, but isn't desirable.
Already in JavaScript: Object.seal(object)
> "This prototype will never be modified at runtime"
Already in JavaScript: Object.freeze(myPrototype)
[0] https://docs.google.com/document/d/1Qk0qC4s_XNCLemj42FqfsRLp...
[1] https://groups.google.com/g/strengthen-js/c/ojj3TDxbHpQ/m/5E...
I clarified in another thread, but what I meant to convey is a way of giving very specific hints about specific pieces of one codebase:
"Function X will only ever take two arguments, they will be of types Y and Z"
"This object will only ever have these properties; it will never have properties created or deleted"
"This prototype will never be modified at runtime"
This could guarantee ahead-of-time that certain optimizations - packing an object as a struct instead of a hashmap, for example - can be done, so that V8 doesn't have to spend time speculating about whether or not to JIT something
I have sometimes found a speedup when adding apparently an superfluous |0.
JS VMs don't need more hints from the code. They need guarantees. They already analyze and profile JS, but as long as JS is allowed to be dynamic (and it has to, it won't be JS without it), then they have to keep the complexity and cost of all the extra runtime checks and possible deoptimizations.
If v8 could accept Typescript directly, it probably could pass this kind of information to the JIT directly, too. But the input language is ES6 or something like it, and it has to follow the calling conventions of it. I'm afraid that adorning ES6 with this information would be either brittle or backwards-incompatible.
Like I said above, it could be shipped as separate (optional) metadata files associated with source files, the same way that source maps already work
Also, what do you do about code loading? e.g. scripts loaded from other files at runtime, or eval? Does it throw an error if a third-party script uses a function incorrectly? Or do we assume that metadata is local-use only?
There are a lot of things in JavaScript and also the "browser environment" (e.g. ads, third-party scripts) that can limit the utility of traditional compiler techniques.
Browsers have experimented with hints quite a lot. Nearly all of them have been rolled back, since adding the general strategies to the jit is vastly more useful for everyone, and perform roughly as good or better since they identify the optimization everywhere, rather than only in annotated code.
---
The only ones I'd call "successful" so far have been 1) `"use strict";` which is not really an optimization hint, as it opts you into additional errors, and 2) asm.js, which has been rolled back as browsers now just recognize the patterns.
asm.js did at least have a very impressive run for a while... but it's potentially even more at risk of disappearing completely than many, since it's rather strictly a compiler target and not something humans write. wasm may end up devouring it entirely, and it could happen rather quickly since asm.js degrades to "it's just javascript" and it continues working if you drop all the special optimization logic (which is to its credit - it has a sane exit strategy!)
JS is heavy on function references (closures are one of the most popular JS idioms), so it's not easy to know at call time how you can optimize your code.
I call f(Z, Y). What happens? Does the machine segfault? Probably not.... so you're in "check the types at the callsite" territory. And honestly those kind of boundary checks are the problem, so to speak.
Are we doing static analysis of the entire codebase (a sort of closed-world theory?) You could maybe do something like this for non-exported functions, though at the V8 level that's tough.
I think that there would be possibilities to somehow get systems to be set up in a way to do more cleverness along these lines, but so long as you're taking in stuff from the outside world you end up needing to set up relatively costly checks, relative to the cost of the "default" JS semantics that you're trying to avoid paying.
>"This prototype will never be modified at runtime"
You can already give these hints with Object.freeze/Object.seal
Not really hints though, they enforce a certain behavior. Because hints aren't enough. A JS engine may already assume you'll never change the objects, but it still has to support cases where you then do so anyways.
I don't think these alone would enable any substantial speedups, since the performance issues arise where unknown objects are used.
You can but my understanding is the performance tradeoffs aren't great unless you have a workload that especially benefits from it. Those calls are expensive.
With this standard we would have standard dynamic JavaScript as the world knows it today, a restricted subset ('constraint spec') that is still designed for human readability/writability, and then asm.js/WebAssembly, which would not be written directly but instead would be an output of code written in other languages. Programmers will want interoperability between all of these paths, and that is a lot of complexity for these engines to manage.
My impression of WASM was it's ok but it's not very expressive for an assembly language - at least it has integers though. gcc.godbolt.org doesn't give you any library functions for it, even memcpy(), so I couldn't do a lot of testing.
> With Liftoff and TurboFan, V8 now has two compilation tiers for WebAssembly: Liftoff as the baseline compiler for fast startup and TurboFan as optimizing compiler for maximum performance.
> Immediately after Liftoff compilation of a module finished, the WebAssembly engine starts background threads to generate optimized code for the module.
It's definitely not uncommon though. Just to provide one example: how often do you use all the arguments to `.map` callback?
Usually you'd write something like `arr.map(x => x + 1)`.
However, to spell out all the arguments you'd have to write `arr.map((x, i, arr) => x + 1)`.
And there's tons of other functions like that in stdlib or 3rd-party libs that people use without passing or accepting all the args. That's why this optimisation is so useful.
I know I would love to see TS type annotations made syntactically valid if semantically ignored (ala Python's type annotations PEPs), simply to make sure valid TS runs in the browser without a build step to better unify the "subset". It wouldn't be a far shot from there to seeing at least some small type annotation hints support in JS engines. Though I definitely understand the concerns and wariness that anything done there would be very engine-specific and in danger of lock in for the web.
I heard there were some V8-specific prototype attempts from some of the Deno developers, but I don't recall how far those attempts got or if they had anything in the way of results.
That seems an insane overhead for the common case... If this really does make benchmarks faster overall, it suggests an even better solution might exist out there that does not add overhead for the common case...
Perhaps it might be worth keeping track of how many arguments a function is typically called with, and recompiling code for each case? Then all the checks for argument counts can be entirely removed in the optimized code.
Maybe I’m wrong — could you elaborate how you’d do it?
Their numbers seem cherry picked or is Turbo Fan actually unable to inline code?
In the article, it appears to be the opposite. Previously this math was required (`[ai] = 2 + parameter_count - i - 1`), but by reversing the arguments in the stack it's now always a constant offset, and they prevent indexing out of the passed arguments in the frame by ensuring there's at least as many arguments as formal parameters by stuffing the call with extra `undefined`s:
> But what happens if we reverse the arguments? Now the offset can be simply calculated as [ai] = 2 + i. We don’t need to know how many arguments are in the stack, but if we can guarantee that we'll always have at least the parameter count of arguments in the stack, then we can always use this scheme to calculate the offset.
Math is fast. Stack space is rarely a key constraint. On the other hand, tracking typical argument counts for a function also requires overhead, and compiling multiple copies of a function is also not free. If a particular call site is hot enough that call overhead matters, it's probably hot enough that inlining is the correct answer.
As one additional data point: V8's new approach appears to be very similar to how SpiderMonkey's been implemented all along. Convergent evolution is generally a good sign.
We spend a lot of time arguing with the cpu. If we give it something it’s actually happy doing, it almost doesn’t matter how stupid that thing is, because it’s stupid fast.
https://www.scalyr.com/blog/searching-1tb-sec-systems-engine...
Where are these programs not using the CPU cache?
I suspect that on modern architectures these trivial additions are effectively free most of the time. In my experience the hard part of optimizing for modern architectures is compensating for memory latency (cache optimization, prefetching) and pipeline flushes (branch prediction).
An inline, unconditional add is as cheap as it gets these days. A few additional bytes of stack that's (almost) always going to be super hot in cache is not really significant.
I suppose it could pose an issue for super deep function call tree where it would cause the stack to grow more than necessary, but given the usual memory overhead of JS I doubt that it's going to be very significant even then.
On machines with 64 bit addressing. There are lots of things that don’t work so well on 32 bit or 16 bit OSes that we have to unlearn.
There have been a couple times where I thought about trying to catalog all of the things I think I know about a piece of hardware, software or even a single library, so that I can challenge my own assumptions when a major release comes out. Never have gotten around to it.
Stack space is virtually free on 32 bit systems as well. V8 is not designed for or portable to 16-bit or 8-bit systems, so while your point is interesting, it’s irrelevant here.
How do you figure that, given we’ve had quite a bit of time wrestling with 2G memory limitations and how those interact badly with multithreading?
If your stack is reaching a significant proportion of 2GB you are seriously doing something wrong and it’s likely other parts of your application will start breaking as well.
It's not 'my stack'. It's all stacks. Each thread has a stack. They are all in your 1.2-2GB address space. If you're not using a Global Interpreter Lock'ed language, you could easily end up with hundreds of threads for production code. And the more you try to dedupe work, the more threads you are likely to end up with.
Given 32 bit words you'll see 4 bytes per call for parameter counts. A call stack 100 calls deep will incur 400 extra bytes of stack space overhead; less than half a KiB. Along the way all extra 'arguments adaptor frames' are eliminated, so the net is even smaller.
That doesn't look like a problem. If you're 100 calls deep and you blow out the stack because it's 400 bytes larger you have other issues.
Most programming languages need the stack to be a linear set of addresses. So you figure out what the max reasonable stack size is for a given thread, and you pre-allocate that address space. Which means even if the stack stays shallow, you can't use those addresses for heap. If you increase the average case for your call stack size, then the available heap shrinks.
That's why I'm saying we are doing this now, even though in theory this knowledge has been available to us for a very long time. Nearly everything is 64 bit now, and even in the JVM where they carve 2 bits off for parallel mark and sweep, there are still oceans of extra address space for us to 'waste'.
The increase in stack usage is highly unlikely to require more space than the default 'reasonable' stack size already being allocated for threads in 32b environments where one is likely to be operating v8; typically the defaults are already pretty generous. Unless the default thread stack size allocation actually has to be increased to accommodate the modest growth caused by the change then there is zero impact on available heap.
A lost opportunity to mention “with this one weird trick”
Ultimately V8 is almost an ecosystem in itself, it's embedded into quite a few other software, the ECMA262 standard is continuously changing, the Web is changing, Chrome changes on top of all this, plus there is a lot of backward compatibility stuff.
You can look at the V8 bugtracker pick a task and try your luck.
One thing that really impressed me is how crazy fast golang is. Not just at runtime but compile time too. Esbuild is like 20x faster than webpack. That’s nuts. I see a webserver running on 512MB ram and single core, serving 10s of thousands of requests a second without batting an eye. Just rock solid reliability and performance.
I want to build web apps with low memory, cpu, and bandwidth profile. Give us a simpler language that doesn’t need crazy complicated hotspot profiling JITs. Just make the default, simple thing insanely fast.
Give us better fundamental primitives that don’t need a gajillion build tools.
The JS ecosystem is so complicated nowadays. It just seems we’re patching shit on top of each other without reasoning from first principles.
Disclaimer: I am not saying this specific improvement isn't worth it. I love v8, but I feel we can make magnitudes of higher performance gains by removing things and simplifying such that you don't even need top level complex optimizations.
What makes you think it isn't? What if I told you that people were creating Electron-style apps over a decade before Electron ever existed? Go use Firefox 0.8 or Netscape 6. Pretty much every part of the UI on your screen that you'll interact with is backed by JavaScript. Keep in mind that this was 15–20 years ago, pre-Windows Vista, where main memory wasn't measured in gigabytes. And that was all pre-JIT!
Here's a screenshot of the file listing for the version of AOL Instant Messenger that was built into Netscape Navigator: https://imgur.com/a/ztaMVon
And a shaky cellphone video of someone using it over 10 years later, before AOL shut down the network: https://www.youtube.com/watch?v=apyR0bPnFAo
> Esbuild is like 20x faster than webpack.
The problem is webpack and the development style favored by the NodeJS/NPM community. It's filled with bad practices, and they all do it because either no one knows better, or they don't care. JS is not inherently slow. It's exceptionally fast, even. Contrary to popular belief, though, how you write code still matters. The myth of the sufficiently smart compiler (including JITs) is that: a myth.
Just serve your ESM modules directly from the filesystem. You don't need webpack or snowpack. Build in 0 seconds.
You also don't need a transpiler like Babel if you drop support for IE11. Almost all ES6 features– such as classes, proxies, arrow functions, and async are supported by modern browsers and their older versions. As long as you avoid features like static, private, and ??=, you get instant compile times.
It's not 2015. You don't need a transpiler to write modern JavaScript. As of writing, the latest version of Safari is 14 and the latest version of Chrome is 88. These features fully work in Safari 13 and Chrome 70, which have less than 1% of marketshare.
> The JS ecosystem is so complicated nowadays. It just seems we’re patching shit on top of each other without reasoning from first principles.
I agree. I don't use any third-party JavaScript dependencies, except for the occasional framework. You can rewrite many third-party libraries yourself and more specialized to your usecase quickly. Third-party JS libraries tend to have poor performance.
You can accomplish a little bit of this with an IDE that supports JSDoc syntax. It will get type hints from your JSDoc definitions and show warnings or errors when you violate those definitions.
This is super useful for Node.js - I've had PRs with features I didn't land because of this and I intend to investigate again e.g. https://github.com/nodejs/node/pull/35877
Made sense 50 years ago... makes sense now.
FredW
https://es.discourse.group/t/callback-based-simplified-async...
Can I assume that V8 moved that gain to JS?
I'm relatively new to Javascript. I've been bitten by both of these recently. I wasted an hour where I added a third argument to a function but missed a place elsewhere that was sending only two parameters. That makes it harder to change your code. Now I read that it's not only a good way to introduce ugly bugs, but that this wonderful feature also makes your code run slower. Genius.
If you want to reduce the amount of mistakes made, in TypeScript accidental under-application becomes a type error. I feel the experience of writing TypeScript is a lot smoother than JS alone.
1. Declare another function with same name and make the older function a wrapper with a default for new argument
2. Support for an explicit default argument
Either of these would have prevented GP from having their phone bug/problem
If you’re trying to change the signature of an existing function, I don’t think we’ve yet figured out a better safeguard than static typing.
When you add TypeScript into the mix, it can help a lot as it is often able to detect under- and over-application as type errors (though not always, due to intentional looseness in the language; TypeScript is not fully sound.)
Here is one notorious example:
[ '1', '1', '1', '1' ].map( s => parseInt(s) )
This returns the expected result: [ 1, 1, 1, 1 ]
Then you think, "I don't need that little wrapper, I can just pass parseInt as the callback directly": [ '1', '1', '1', '1' ].map( parseInt )
And this returns something that you may not expect: [ 1, NaN, 1, 1 ]
That happens because parseInt() takes an optional second parameter with the number base, and map() passes a second parameter with the array index. Oops!A similar situation can arise when you add a new optional parameter. You still have to check all the call sites, and for a public API you won't be able to do that.
1. https://www.typescriptlang.org/docs/handbook/intro-to-js-ts....
I fully understand that the JS community has a thing for dependencies, and views Amazon as its representative use case, but I was hoping to add a few small Javascript functions to a static html page. JS loses its appeal quickly when you start adding things like that - if I can't open an html file in a text editor and add the functions I need, I might as well use a different language.
This is not a "JS community has a thing for..." bit, this applies to any programming language.
But anyway, if you want to avoid a compilation step, the link GP mentioned shows how TypeScript can check JS files annotated with normal comments. You can run it through the a compiler to remove the annotations if you want, but it's absolutely not required.
It seems like more than a programmer should have to do, but eventually you'll forget about the extra tools and have decent pseudo-compile-time error detection.
['1', '7', '11'].map(Number)
Using `Number` is probably safe because it'd break much of the web of it were changed. Also note that it is not equivalent to parseInt, as Number and parseInt apply different parsing rules.
['1', '7', '11'].map(function(n) {return parseInt(n);})
but no, map has got an extra argument, the index of the array function traceParseInt(n, i) {
console.log(i);
return parseInt(n, i);
}
['1', '7', '11'].map(traceParseInt)
0
1
2
[ 1, NaN, 3 ]
Nice way to wait for an integration test to complete. Thanks.(Which also means that a static type checker would not have caught this "bug".)
How? Sure, if you make a breaking change it requires you to hunt down all callers, but JavaScript’s dynamic nature does that anyway.
It makes a lot of things easier, too, though it's not without gotchas (the most frequently encountered, IME, one being functions that you normally only use a subset of the arguments combining with functions which take callbacks where you normally use only a subset of the passed arguments interacting in surprising ways; the sibling comment on the map/parseInt combo being a good example.)
In situations like single-page app building, it's very useful.
[1]: Java does have variadic functions, but we also know at compile time which functions have such a signature, so it probably just desugars to a normal array as the final argument.
function print(arg) {
console.log(arg);
}
print("a", "b", "c");
It’ll only output "a", obviously. In fact, this will run just fine too: print();
You’ll get "undefined" in the console, but it’ll work.JavaScript’s nature is that arguments are in the "arguments" pseudo-array, and the parameters are just fancy names for different values in "arguments". See:
function count() {
console.log(arguments.length);
}
count();
count("a");
count("a", "b");
In order, you’ll get: 0, 1, and 2. Despite the fact that "count" has no parameters in the definition.In the first function ("print"), "arg" is just syntax sugar for "arguments[0]".
What I’m getting at is: in C, Java, etc., the compiler knows what is a varargs function and what isn’t. In JavaScript, the interpreter/compiler doesn’t and has to assume everything is.
So much so that they reflect one another: you can set `arguments[0]` and retrieve that value from `arg`, and the other way around:
function foo(arg0, arg1) {
arg0 = "foo";
arguments[1] = "bar";
console.log(Array.from(arguments), arg0, arg1);
}
foo(1, 2)
will log [ "foo", "bar" ] foo bar
Although in reality optimising engines treat `arguments` as a keyword of sorts: they will not reify the object if they don't have to. However this meant they had to deoptimise a fair bit before the "spread" arguments arrived as that was how you'd call your "super".That's syntactic sugar for an array, in the most literal way possible: when you define `func(Object… args)` the actual bytecode is that of `func(Object[] args)`, and you can pass in an array to stand in for the entire varargs.
So no, they're right. The varags just counts as 1 formal parameter.
public static void main(String[] args)
public static void main(String... args)The JVM isn't limited to Java, for example: nashorn, jruby, groovy
Hence the optimization would make sense for the JVM for those client languages: nashorn, jruby, groovy, etc
Quite trivial to understand, isn't it?
Edit: off topic but I randomly stumbled on upon an interesting comment about CoreCLR and it happen that it was yours (2019), do you have news regarding the development/improvements of RyuJIT?
Graal JIT compiler as used in OpenJDK, is a subset of GraalVM, and it has been such a burden to keep in sync with proper GraalVM, that it was decided to drop it and invest into improving C2 instead.
https://bugs.openjdk.java.net/issues/?jql=labels+%3D+graal
Second, nashorn tricks with invokedynamic and pretending to be Java never worked to the extent achieved by V8, hence why its development was dropped and everyone got told to migrate to GraalVM JavaScript.
https://www.graalvm.org/reference-manual/js/NashornMigration...
Third, JRuby always suffered to pretend to be Java to achieve acceptable performance on the JVM, even with invokedynamic.
"JRuby: The Hard Parts with Charles Nutter"
https://www.youtube.com/watch?v=RvUouqLxgrY
And Graal uses a completely other execution model for Ruby, TruffleRuby, https://www.graalvm.org/reference-manual/ruby/
As for CLR, yes there have been plenty of progress, .NET Native (which uses VC++ C2 compiler), CoreRT, and now the upcoming .NET 6 AOT compiler that is supposed to replace them.
There are a series of blog posts related to .NET 5 improvements, including C++ runtime code being replaced by modern C# features, and future roadmap.