Why is the number of local variables used in a Java bytecode method not the most economical?
Asked Answered
R

5

31

I have a snippet of simple Java code:

public static void main(String[] args) {
    String testStr = "test";
    String rst = testStr + 1 + "a" + "pig" + 2;
    System.out.println(rst);
}

Compile it with the Eclipse Java compiler, and examine the bytecode using AsmTools. It shows:

byte code

There are three local variables in the method. The argument is in slot 0, and slots 1 and 2 are supposedly used by the code. But I think 2 local variables are just enough — index 0 is the argument anyway, and the code needs only one more variable.

In order to see if my idea is correct, I edited the textual bytecode, reduced the number of local variables to 2, and adjusted some related instructions:

edited

I recompiled it with AsmTools and it works fine!

So why don't Javac or the Eclipse compiler do this kind of optimization to use the minimal local variables?

Roderick answered 28/7, 2019 at 15:58 Comment(2)
If you had made the first string final it would have been less of everything.Trifocals
I just started writing an answer, suggesting that minimizing the number of local variables might just be a hard problem ("hard" meaning "NP-hard" here). This would have been based on my intuition, hal-lara.archives-ouvertes.fr/hal-02102286/file/RR2006-13.pdf and the paper that it refers to. But I'm not familiar enough with that to go out on a limb with that one. Maybe someone else would like to say a few words about the theoretical background and complexity of this sort of optimization problem...?Benedicto
Z
25

Simply because Java gains performance from the just-in-time compiler.

What you do in Java source, and even what shows up in the class files isn't what enables performance at runtime. Of course you shouldn't neglect that part, but only in the sense of not making "stupid mistakes".

Meaning: the jvm decides at runtime if a method is worth translating into (highly optimized!) machine code. If the jvm decides "not worth optimising", why make javac more complex and slower by having a lot of optimization in there? Plus: the more simple and basic the incoming byte code, the easier it is for the JIT to analyze and improve that input!

Zurn answered 28/7, 2019 at 16:7 Comment(3)
"What you do in Java source, and even what shows up in the class files isn't what enables performance at runtime." - This is true to an extent, especially in OPs case. However, some JIT features do depend on how the code is written, such as escape analysis, which helps determines eligibility for other optimizations (such as scalar replacement). Source code matters even when it hits javac (auto boxing pitfalls, string builder pitfalls, etc..). I wouldn't recommend people rely too much on JIT to clean their code, rather they should avoid pre-optimizing as OP is doing, focus on real problems.Jeminah
@VinceEmigh Sure. The JIT isn't about fixing stupid mistakes on the programmers side. I see it the other way round: you avoid the stupid mistakes, you write, clean, simple code, and then you let the heavy lifting happen at runtime. And if things aren't performing as expected or required, you carefully measure what is going on.Zurn
What you wrote in the last comment is confirmed by one of the top authorities here: Write Dumb Code by Brian Goetz. Although the article is 10 years old now, it might be the case that one statement from the article that refers to 2003 can be applied here iteratively: "It's truer today than it was four years ago". The JIT is a beast, and everybody who had a look at its optimization techniques and their results would be humbled and hesitate to claim that he could do it better...Benedicto
E
40

There are several reasons. First off, it's not necessary for performance. The JVM already optimizes things at runtime, so there's no point in adding redundant complexity to the compiler.

However, another major reason noone has mentioned here is debugging. Making the bytecode as close to the original source as possible makes it a lot easier to debug.

Emmalineemmalyn answered 28/7, 2019 at 16:14 Comment(23)
Yes. Byte code is not an executable code. It will be interpreted before execution. The executable code much be much different.Nason
@Nason technically it could be directly executable, but the performance probably wouldn't be great.Vivianne
@Nason Unless you have a Java Non-virtual Machine instead of a JVM. Then suddenly the Java Byte Code is executable.Flavorous
@mentallurg: "Yes. Byte code is not an executable code. It will be interpreted before execution. The executable code much be much different." – That makes no sense to me. Interpretation is execution, therefore byte code is executable code. I mean, Java byte code is literally what gets executed by the JVM, so how is "not an executable code" if it is literally the code that is being executed?Scopas
@OrangeDog: "technically it could be directly executable, but the performance probably wouldn't be great" – Actually, almost all JVMs do directly execute byte code precisely because the performance is superior for certain usage profiles.Scopas
@JörgWMittag no they don't. Intel and ARM chips cannot directly execute JVM bytecode.Vivianne
@OrangeDog: But JVMs can. And they do. For example, HotSpot gets its name from the fact that it only compiles pieces of code that are performance-sensitive and directly executes the rest.Scopas
@JörgWMittag direct execution would not involve a JVM at all.Vivianne
@Vivianne What exactly do you mean by "direct execution?"Flavorous
Bytecode is technically executable, @JörgWMittag, provided a means of translating it to machine code, but not officially considered executable because it's really just half-compiled code and is not, in fact, what gets executed by the JVM. During actual execution, the JVM runs bytecode through a JIT (just-in-time) compiler, optimising and finishing the compilation process during execution. It provides significant performance gains over interpretation (albeit causing more system load), while allowing for platform-agnostic "compiled" code to be distributed.Scree
(Yes, this means that a Java program is basically one of those pre-baked frozen pies you can buy; just pop it in the JIT compiler for 10 minutes, then eat it!)Scree
@JustinTime Oracle seems to disagree: "Adaptive compiler: A standard interpreter is used to launch the applications. When the application runs, the code is analyzed to detect performance bottlenecks, or hot spots. The Java HotSpot VM compiles the performance-critical portions of the code for a boost in performance, but does not compile the seldom-used code (most of the application)." (emphasis mine)Flavorous
Oh, that's interesting, @8bittree. I was certain they'd redesigned the JVM to JIT compile instead of interpret, not use a combination of both. That's pretty useful to know.Scree
@JustinTime Also, the difference between bytecode and machine code is much, much smaller than you think. After all, I can take Java bytecode, and run it natively in hardware, as I pointed out above (there are more examples of Java processors here ). And I can take ARM machine code and run it in a VM like qemu on an x86 machine with no native ARM support.Flavorous
@Flavorous But the fact remains that Java bytecode isn't a great platform for direct execution. As seen: it isn't highly optmized.Zurn
@Jörg W Mittag: "interpretation is execution" - no. Execution is what CPU does. CPU excutes the code. Executable code is the code that particular CPU can execute. Interpreter converts code from some format to the format that CPU can execute. JVM interprets .class files and creates executable code for CPU. Visual Basic interprets. vbs files and creates executable code.Nason
@mentallurg: "Execution is what CPU does." – Yes, because a CPU is an interpreter. (Well, today's CPUs sometimes actually contain compilers as well.) Execution and interpretation are really just two words for the same thing. "Interpreter converts code from some format to the format that CPU can execute." – Huh? An interpreter never ever "converts" something. What you describe is a compiler. In fact, what you describe is the very definition of a compiler: something that converts a program from one language to another language is, by definition, a compiler.Scopas
@Jörg W Mittag: That is funny :) Sorry, but CPU is exactly the one who executes the code. And no, interpretation and execution have very different meaning. Interpreter converts some code to the format the particular CPU can execute. The main difference between interpreter and compiler is, that compiler analyzes the whole source, where as interpreter analyzes consequently only small part that it needs on the next step (e.g. 1 line of Python code at a time, or a single Java instruction in .class file at a time). Neither compiler nor interpreter executes the code. The CPU executes the code.Nason
@mentallurg: "Interpreter converts some code to the format the particular CPU can execute" – That is wrong. An interpreter never ever "converts". Only a compiler does that. In fact, that is the very definition of a compiler: something that converts code from one language to another is a compiler. By the very definition of the term "compiler". An interpreter evaluates the result and performs the side-effects of a program. Or, in other words, it "executes" it. A compiler converts the program to a semantically equivalent program in another language. Where "semantically equivalent" means …Scopas
… that interpreting the compiled program with an interpreter for the output language will evaluate the same result and have the same side-effects as interpreting the original program with an interpreter for the original language. (Where "same result" and "same side-effects" are relative the language specification, of course. For example, usually the compiled program is not required to have the same performance.)Scopas
@mentallurg: In both of the examples, my friend is a compiler, and in both of the examples, I am an interpreter. It's easy to see when you just look at the definitions of the two terms: if it translates, it is a compiler. In both your examples, my friend translates, ergo, in both cases, my friend is a compiler. If it executes, then it is an interpreter. In both your examples, I execute, ergo, in both your examples, I am an interpreter. My friend would only be an intepreter, if he were the one cooking by directly reading the Chinese recipe without translating it.Scopas
Note that your example is very confusing, because the word "interpreter" as a job description means exactly what you describe in the second example, namely it is a human who translates something while it is happening as opposed to a "translator" who translates something after it has happened. In programming however, an interpreter is something that executes and a compiler (which is actually also sometimes called a "translator") is something that translates. So, what an "interpreter" in the standard English sense does would actually be a "compiler" in the programming sense.Scopas
@Jörg W Mittag: Thank you. Now it is clear what your problem is. You apply to the words a meaning different from what is used in the IT industry. Don't be surprised if the most software developers will not understand you. I tried, but I cannot help you.Nason
Z
25

Simply because Java gains performance from the just-in-time compiler.

What you do in Java source, and even what shows up in the class files isn't what enables performance at runtime. Of course you shouldn't neglect that part, but only in the sense of not making "stupid mistakes".

Meaning: the jvm decides at runtime if a method is worth translating into (highly optimized!) machine code. If the jvm decides "not worth optimising", why make javac more complex and slower by having a lot of optimization in there? Plus: the more simple and basic the incoming byte code, the easier it is for the JIT to analyze and improve that input!

Zurn answered 28/7, 2019 at 16:7 Comment(3)
"What you do in Java source, and even what shows up in the class files isn't what enables performance at runtime." - This is true to an extent, especially in OPs case. However, some JIT features do depend on how the code is written, such as escape analysis, which helps determines eligibility for other optimizations (such as scalar replacement). Source code matters even when it hits javac (auto boxing pitfalls, string builder pitfalls, etc..). I wouldn't recommend people rely too much on JIT to clean their code, rather they should avoid pre-optimizing as OP is doing, focus on real problems.Jeminah
@VinceEmigh Sure. The JIT isn't about fixing stupid mistakes on the programmers side. I see it the other way round: you avoid the stupid mistakes, you write, clean, simple code, and then you let the heavy lifting happen at runtime. And if things aren't performing as expected or required, you carefully measure what is going on.Zurn
What you wrote in the last comment is confirmed by one of the top authorities here: Write Dumb Code by Brian Goetz. Although the article is 10 years old now, it might be the case that one statement from the article that refers to 2003 can be applied here iteratively: "It's truer today than it was four years ago". The JIT is a beast, and everybody who had a look at its optimization techniques and their results would be humbled and hesitate to claim that he could do it better...Benedicto
U
6

Well, you did just make a false dependency between what used to be two completely separate locals. This would mean that the JIT compiler either needs to be more complex/slower to unravel the change and get back to the original bytecode anyway, or would be restricted in the kind of optimizations it can do.

Keep in mind that the Java compiler runs once, on your development (or build) machine. It's the JIT compiler that knows the hardware (and software) it's running on. The Java compiler needs to create simple, straight-forward code that is easy for the JIT to process, and optimize (or, in some cases, interpret). There's very little reason to excessively optimize the bytecode itself - you might shave off a few bytes off the executable size, but why bother, especially if the result would be either less CPU efficient code or longer JIT compilation time?

I don't have the environment to do an actual test right now, but I'm pretty sure the JIT will produce the same executable code from the two bytecodes (it certainly does in .NET, which is in many ways similar).

Universalism answered 29/7, 2019 at 11:11 Comment(1)
The HotSpot JVM's JIT compiler uses the Static single assignment form to represent all things that happen with the local variables and the operand stack, so it couldn't care less about the number of variables used here. Or, in other words, your assumption is right, it will likely produce identical native code when not needing to support debugger access.Limbate
P
1

There's also a problem with bytecode verification. You know that every variable in Jave must be defined before it can be used. If you merge variable x and variable y together, and the order is "define x, use x, use y" that should be detected as an error by the bytecode verifier, but after merging the two variables it would not be detectable anymore.

As an optimisation, it's better left to the just-in-time compiler, which can decide which variables it wants to share space.

Proceleusmatic answered 29/7, 2019 at 10:54 Comment(1)
That's not a real issue since the Java compiler has to check stuff like that at compile time anyway.Emmalineemmalyn
R
1

The promise of java, is that the same code can run on multiple systems. Java could optimize its bytecode right from the start. But it prefers to wait until all facts are known.

  • Hardware: The same bytecode could run on a raspberry pi or on a multi-core unix server with 64GB.
  • Usage: Some functions are hardly ever called and others are called several times per second.
  • Flexibility: in the future the bytecode could run on a different JVM, which offers new optimizations. (JDK x ?)

So, by postponing decisions, bytecode can be restructured and finetuned even better, with respect to all these variables.

Conclusion: Don't rename/move/eliminate variables just to make code faster.


Why usage is so important:

Java keeps track of which methods are called most often, and which flows are followed most often through the code.

One possible optimization then is "Method inlining" which means that entire methods are not just restructured but merged together. Once you merge methods together, you can work on bigger blocks of code, and optimize even better. You can actually eliminate variables even more, reusing them throughout entire flows.

Redbud answered 30/7, 2019 at 14:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.