Hacker News new | ask | show | jobs
by chrismorgan 2053 days ago
> const performs a lot better in optimized code than var or let

This puzzles me; if only ever one value is assigned, I would have expected at least let to perform identically to const in optimised code, because I expect the optimiser to look at the let and say “never reassigned, turn it into a const”. By the sound of it, I’m wrong, and I’d be interested to know why I’m wrong.

5 comments

JavaScript is very dynamic. Here I define a variable with `let` and then change it:

let foo = 10; eval("fo"+"o = 10");

I could "obfuscate" that eval assignment as much as I like. So you can't completely statically analyse `let` variables.

That said, it's likely you'd end up with perf very close to `const` in a very "hot" part of your code, since a good JIT compiler like V8 will eventually make "assumptions" about your code, and optimise around them, while having (ideally cheap) checks in place to ensure the assumptions continue to hold.

An interesting point, however direct "eval" is a special case.

It's already known that functions containing direct "eval" are not subject to the same level of performance optimisations as other functions. There is no way to obscure the call to direct "eval" itself; the compiler knows clearly whether it occurs.

Without "eval" appearing syntactically inside a function's scope, there is no dynamic access to "let" variables, and there's no need for the JIT code to check the assumption at run time.

Despite no other assignments, the "let" variable does change value: It has the assigned value after the "let", and the "temporal dead zone" value before it in the same scope. However "const" also has this property so it's not obvious why there would be a speed difference.

> There is no way to obscure the call to direct "eval" itself; the compiler knows clearly whether it occurs.

I must be misunderstanding you, because you've stated that very confidently.

window["e"+"val"](...)

> There is no way to obscure the call to direct "eval" itself;

Jsfuck would like to have a word with you

Nice try! The Jsfuck method can't encode a direct "eval", only an indirect one :-)

Thus the emphasis on direct. Well, Jsfuck can encode a direct eval inside an indirect eval, but that doesn't give it any advantages, it still can't access the surrounding lexical environment in the form of "let" and "const" variables.

(I didn't know about Jsfuck though - it looks fun, thanks!)

Does it actually behave like having a literal eval in the function?
Doesn't JSFuck only work when strict mode is off?
These checks can be completely free, in terms of runtime cost, through deoptimisation.
How? You have to do some kind of check with let, and you don't with const.
Not sure if V8 does this, but a general strategy used in HotSpot is to compile assuming the good case and install a "trap" to deoptimize the code if the condition is violated. So you could compile the let like a const with no checks on the use of the variable, but have a trap such that if any code ever tries to assign to that variable, all code compiled under the assumption that it was constant is throw out.
The const keyword also guarantees that once a value is assigned to its slot it won't change in the future. As a result TurboFan skips loading and checking const slot values slots each time they are accessed (Function Context Specialization)

https://github.com/thlorenz/v8-perf/blob/master/language-fea...

Right... but the question was 'why can't the same thing be done with let'.

The answer is probably a combination of 'they haven't gotten around to it yet', 'they don't see the need', 'they don't want the complexity', and 'they don't want to spend compile time doing that.'

Because you cannot know whether the let variable is changing just with static analysis without running the associated code, right?
In strict mode you can because you can statically determine if eval is present. If it isn't, it is trivial to determine if it is written to after initialization.

Otherwise it has the same issues as const (is there a temporal dead zone violation?)

> just with static analysis

You're not doing static analysis - you're doing dynamic analysis and speculation. But this has the downsides I said - it's more complicated, it takes more time, etc.

What speculation? If you have a "let" in some lexical scope, AND this is strict mode, AND there are no calls to eval in that scope, AND there are no assignments within that scope... how is that different from const?

I can understand why in a JavaScript engine there may be plenty of other concerns and it may be completely reasonable to not do the analysis, but in the common case it should (at least) be something you could do just by walking the AST, if you so desired, without even knowing the surrounding code.

> What speculation?

Speculating that a debugger is not attached and modifying local variables, is one example.

> in the common case it should (at least) be something you could do just by walking the AST

Yes that’s why it’s speculation - handle the common cases and speculate away the uncommon cases.

The question needs to be "why on earth would you want to do that with let, when you should just make it a const?"
Because minifiers can shave a few bytes by converting a mixture of const/let to use just let.
I think you may have misread my comment?
Because such analysis isn't free, especially when it's just in time. In theory it could and maybe will in the future, but it's hard to do every possible analysis and optimization.
The doc is incorrect, `let`(and even `var`) does perform as well as `const` in optimized code (but it comes with performance cliffs, if there is a re-assignment).
Wouldn't "looking at the let" take some cpu time?
"Looking at the let" refers to code analysis during JIT compile time, which happens once for each bit of code, not run time which happens many times as the same bits of code are run.

When comparing the speed of "const" versus "let", the JIT compile time is irrelevant; the speed differences being looked at are entirely run time, inside loops.

Also the JIT compile time difference from "looking at the let" will be so low as to be virtually unmeasurable anyway. (It is such a trivial check, much simpler than almost everything else the compiler does.)

(However, see rewq4321's sibling comment about analysability and JavaScript being a very dynamic language when "eval" is used.)

FYI - you're probably getting downvoted because JIT-time is generally considered runtime and the cost of JIT time is usually consider with end user performance in mind.
> the cost of JIT time is usually consider with end user performance in mind.

Indeed it is. I know that (very well), I guess I didn't phrase my comment well though.

The article points out that a "const" variable access is faster than a "let" variable access due to removal of certain run time checks in the JIT-generated code.

The per-statement cost of lexing, parsing, analysing, and code-generating for each statement of the JavaScript source is far, far greater than the cost of actually executing the generated code for a "const" or "let" variable access.

So much so, that if the generated variable access was run as a one-off, the speed difference would be undetectable.

So the only way for there to be an end-user visible difference in speed between "const" and "let" is in code where the JIT-code-generated variable accesses are run many times repeatedly.

The time to perform "is the 'let' variable assigned to" is part of the one-off checks, not the repeated executions. Since we're also talking about "optimised code" not "unoptimised code" from the JIT, it's also a tiny amount of time compared with most other things done by the optimiser. It is literally a boolean flag, false by default, set true if any assignment to the variable is seen when parsing or optimising. And "optimised code" is only produced for code that is run many times.

So technically, yes, the check would technically take end user-visible time. But only in situations where there is another end user-visible time which dominates over it (the slowdown of "let" versus "const" variable accesses done repeatedly), and which the check is intended to remove. Thus you could say, the check's time would be cancelled out.

(It's a little bit like comparing the O(1) parts of an algorithm with the O(N) parts, when you have been told that N is significant. If you can do something to reduce the O(N) constant factors and it costs O(1) to do, it's a net speedup for sufficiently large N.)