A New Bytecode Format for JavaScriptCore
webkit.org
webkit.org
Seems like with JS you would be constantly transforming from bytecode -> AST -> bytecode -> machine code, such as every time a method adds a new field to an object or some optimization assumption is violated. It doesn't seem like it would be easier or faster to work with. I'm guessing there wouldn't be a whole lot of memory benefits either.
Would someone mind illuminating me on this design choice? (Granted, I've not taken a compilers course, so feel free to call me an idiot for not knowing something basic about how compilers work).
I have no idea if this is the reason, but it's a possible one.
(I don't know JSC does it so that could be wrong)
ASTs are a pretty annoying form to use as a source of shared truth for OSR. JSC has 4 tiers so we need a convenient-to-use shared truth IR. That’s what bytecode is for. For example, bytecode offers high-scalability answers to questions like “where should I exit” and “what is live when I get there”.
Compiler IRs tend to have the property that operands are not themselves expressions; they can only be variables or maybe constants. That makes it easy to identify sequence points, which makes it easier to insert code or reshape the control flow graph.
Serializable format of AST is a hard problem. WASM was supposed to be like that, but in the final design it became a stack machine.
* You don't translate from bytecode to AST.
* In tiered JIT architectures--typical for dynamically-typed languages like JavaScript where as you point out effective types can mutate at runtime--most code is executed by an interpreter, not as compiled machine code.[1] Interpreters are more efficient when executing a bytecode than walking an AST.
* Translating an AST straight to machine code is much more difficult than translating a low-level intermediate representation, and such representations are often easy to express as simple bytecodes. Compilers like GCC and LLVM use intermediate representations: RTL and IR. Both are effectively assembly languages, and like all assembly languages trivial to express as bytecode. GCC also has GIMPLE, a higher-level intermediate representation earlier in the pipeline which expresses a generic AST. The fact that GIMPLE and RTL coexist drives home the point that you don't want to be translating tree-like representations straight to machine code.
* You don't need to take my word for this, or the word of anyone else. Every programmer should have experience writing parsers, compilers, and interpreters, regardless of whether they took a class or even went to university. By doing this most of the reasons for the way things are done will become immediately clear to you. Note that splitting strings with regular expressions does not count as parsing, not for these purposes. A good project, however, would be to write a regular expression parser, compiler, and interpreter. There are many examples to follow and you can copy their designs exactly; just don't copy+paste as actually going through the motions and understanding the complexities inherent in the implementations is how everything will become clear.
[1] How this mutation is handled and why tiered architectures are preferable is another topic entirely.
That being said, consistency of internal data structures is probably one of those areas that separates a JIT from an offline compiler. I can see the benefit of switching to a bytecode representation pretty early in that circumstance.
You can use tools like jscodeshift [0] to refactor code! I've previously written codemods to migrate away from a bad design decisions. The React team has also used codemods to introduce breaking changes. AST Explorer [1] is the best tool I've found for writing codemods since it gives you nearly instant feedback and allows for a good amount of exploration.
The majority of developers doing yet another software as a service CRUD app or your typical “dark matter developer” who will write software that will never see the light of day outside of the company they work for would never benefit from writing a compiler or interpreter.
Now if they followed other path into programming, then by all means.
What next they aren’t a “true engineer” unless they can reverse a b-tree on the whiteboard?
Which most likely will have a good class in algorithms and data structures.
So while I don't expect every engineer to actually be able to do it, I expect him/her to be able to explain how to attack the problem.
When they certify the university and then when they issue the professional titles exam.
I am well aware that US doesn't seem to value proper engineering education, and everyone is allowed to call themselves engineers.
Thankfully not every country sees it that way.
Then there is the whole point if HR is hiring actual engineers.
For a very long time JSC did use an AST, and interpreting and executing an AST is expensive. The very first version of the byte code interpreter (all hail squirrelfish) was in the order of twice the speed of the AST, and the AST had been extensively micro-optimized at that point, to the extent that the dispatch cost itself (a single indirect call) was more than 10% of execution time.
There were also a bunch of correctness issues:
* Handling exceptions required manually check call results everywhere. There was an unending stream of minor correctness bugs caused by this. Looping semantics were similarly exciting. Because every call technically required those checks there was a significant cost there, even though there were numerous cases they were missed.
* Changes in language semantics required duplicate changes in many different places
* New language features require a lot of very similar behaviour, with minor variations that resulted in duplicated code, introducing new places to accidentally miss exception and loop checks.
* Memory overhead was massive, both in terms of actual usage, and also allocation costs.
* Significant reductions in maximum recursion depth. Back in the days of the AST interpreter JSC had a hard limit of 500 levels of recursion, with plenty of checks, and it would still periodically blow the system stack and crash.
The cost of producing the byte code in the first place is super low, and because the byte code is all essentially the most primitive of JS operations, new language features generally don't require the optimizing passes to be aware of any changes, because they're mostly just syntax around existing primitive operations.
I've worked on WebKit memory performance in the past, so I'm well aware that these aren't low hanging fruits we're talking about. The type safety bonus features look great too. :)
(Of course, Apple has their own ARM implementations to consider.)
EDIT: Here’s the paper I was thinking of in this regard:
The negligible performance difference on modern Intel chips between direct and indirect threading is largely a result of their deep, heavily buffered branch predictors. Spectre mitigations are going to increase the performance differences. Most userspace applications will forgo mitigations in favor of keeping performance, but the browser was the big exception as it has to be concerned about in-process side-channels.
I seriously doubt the mitigations in place, whatever they are, are comprehensive even for existing proven Spectre channels. The engines haven't even solved RowHammer. It's a very difficult problem domain, particularly for an application JIT'ing random, untrusted code from the Internet. In many respects they're (IMHO) quietly punting because there just aren't satisfactory solutions. Browsers are really stuck between a rock and a hard place, more so than VM providers like AWS EC2.
One of the more general solutions is simply to stop trusting the browser, if not your entire operating system. Keep your most sensitive secrets inaccessible to software to the greatest degree possible. Use smartcards and other hardware tokens for authentication, for example.
Indirect threading relies more on strong and deep history pattern matching at a single location, and probably uses less state overall.
I remember having a discussion about rowhammer on the v8 team when the original paper first came out - most of us thought it wouldn't be possible to exploit in JS but I suspected it would. Unfortunately I was right :/
I suspect at some point we'll discover WebGL can be used for rowhammering somehow too.
I wonder how performance and memory use compare nowadays between V8, Spidermonkey and JavascriptCore.
Does anyone have a link to recent trustworthy and thorough benchmarks? (Google doesn't really spit out anything noteworthy...)
I don’t know how the VMs stand against each other on memory. JSC is improving in this area a lot recently but I don’t know if it’s just catching up or leaping ahead or whatever.
Unifying is not a bad idea.
I don't think WASM bytecode is a good format for execution, and it's only mediocre as a compilation target.
This is kind of a nonsensical statement. JS itself doesn't have a bytecode, it's just a specification of the language you write scripts in.
Each implementation has its own intermediate representations, including bytecode formats.
WASM is a vendor-neutral standard that is designed as a compilation target and designed with safety in mind.
SpiderMonkey/JSC/V8 internal bytecode formats are just that, compiler internals.
Because then any changes require duplicating the byte code parsers, and adding translation logic to manage those changes.
You must understand: any change would be an API break, and API cannot change. Deprecating an API takes years. You also can't change/add API in security updates, which means if a security fix required changing some aspect of the bytecode that change couldn't be made.
Lets say you change numbering of registers? Thats an API change.
Lets say you change number of opcodes? API change.
Any new API in macOS and iOS requires weeks of review cycle to ensure:
* It solves a problem in a generic manner: e.g. it doesn't solve a very specific version of a general issue
* It can be kept stable: e.g. it doesn't expose implementation details that may change
* It is ABI stable: direct memory access APIs cannot expose layout that can vary based on any internal details.
Shipping API is incredibly difficult if you care about ABI stability for software.
So hopefully https://blog.cloudflare.com/binary-ast/ will become a standard.
Already part of Firefox Nightly! about:config, unrestricted BinaryAST
It's important to realize: binary-ast is no more readily executable than plain JS, it is no more trustable than plain JS, and even if they were parsing is not been a significant part of ttfe in JSC in a long time. It's the interpreter codgen that eats up time, and the execution speed difference between AST (which JSC used for a looonng time, and the final builds beat the contemporary bytecode interpreters of other engines when it was replaced). The performance of an AST is so much slower than bytecode interpreter that you require a tiny amount of JS before the cost of codegen+execution beats just execution using the AST.
Also the bytecode format has no memory safety guarantees so standardizing it wouldn’t be a great idea. It’s only trusted to obey basic VM invariants if we know that we generated it ourselves.
[1] - https://github.com/WebAssembly/proposals/issues/16 [2] - https://github.com/WebAssembly/webidl-bindings/blob/master/p...
Must read.
https://github.com/binast/binjs-ref from below is part of this!
You get pretty good improvements by converting ASTs into SoA representation, interleaving them, and then compressing them. Fast to decode, too.
If you want bytecode, use wasm.
For example for hybrid desktop/mobile apps.
The bytecode is also an internal format so is consider trusted - once you expose it you have to worry about invalid bytecode.
But parsing JS isn't a significant bottleneck, it's the subsequent work to produce the stuff that actually runs. IIRC proponents also claimed it meant you didn't have to worry as much about validation, which isn't true because it's untrusted content that comes from the internet. It must be validated.
binast also is not designed to be any more readily executable that JS - so even if it were supported step one would be "produce a bytecode that is fast".
Time and Time again it seems WebKit are the only team that wants to make the Web with better Web Pages with javascript , All the others seems to want the Web to be Fat Apps.
In the hope of anyone in Safari team is reading it.
Please make the Tab Overview Cache the Thumbnail or in List format, currently pressing Tab Overview will reload all the tabs in the background. I don't know if this is for generating thumbnails or other reason. Something that kills my machine when I have 300 Tabs, most of them are "cold" and not loaded.
I don’t see how this blog post supports that view, since it’s talking about optimizing JavaScript.
"V8 team has built a new JavaScript interpreter, called Ignition, which can replace V8’s baseline compiler, executing code with less memory overhead and paving the way for a simpler script execution pipeline."
"Lazy parsing speeds up startup and reduces memory overhead of applications that ship more code than they need."
https://v8.dev/blog/embedded-builtins
"V8 built-in functions (builtins) consume memory in every instance of V8. The builtin count, average size, and the number of V8 instances per Chrome browser tab have been growing significantly. This blog post describes how we reduced the median V8 heap size per website by 19% over the past year."
https://v8.dev/blog/improved-code-caching
"V8 uses code caching to cache the generated code for frequently-used scripts. Starting with Chrome 66, we are caching more code by generating the cache after top-level execution. This leads to a 20-40% reduction in parse and compilation time during the initial load."
Etc, etc.
"The new bytecode format uses approximately 50% less memory, which means a reduction of 10% in overall memory usage for JavaScript-heavy websites, such as Facebook or Reddit."
This is squarely to help webapps be faster and more memory efficient. This change is great, and doesn't align with the cargo cult of, "Make web pages great again."