0

So, I know that many C-style languages have decrement (--) and increment (++) operators, and allow the mutation to happen before or after the whole expression is evaluated.

What happens when a post-increment happens on a return? I'm not asking in terms of behaviour, but rather implementation.

Given a virtual machine (e.g. JavaScript/JVM) or physical machine (e.g. compiled C++), are the generated opcodes something like the following? (Assuming stack-based arguments/returns.)

int x = 4, y = 8;
return f(++a) + y++;

Turns into this: (Maybe?)

LOAD 4 A
LOAD 8 B

INC A
PUSH A
CALL F
POP INTO C
ADD C BY B
INC B
RET C

If so, how do such operations in these languages decide where to embed the incrementation when the expression becomes complex, perhaps even a little Lispish?

Louis Jackman
  • 1,001
  • 6
  • 16
  • 1
    I think the `INC B`and `ADD C BY B` should be reversed, the post increment to `y` should be applied after the sum. Also, if `y` is a local variable, wouldn't the post increment to `y` be dead code? – Jorge Núñez Mar 17 '13 at 11:29
  • Fixed. But where would it go in a complex expression? Just after, the same? – Louis Jackman Mar 17 '13 at 11:34
  • 1
    It is dependent on the implementation, in an expression like `(x++ + y++) - z++` in C++ for example, there is nothing in the specification that says in which order the post increments should be executed, it will evaluate the sum, the subtraction and then it could evaluate the post increments in any order it wants, or it could even evaluate `x++` after the addition just the same. – Jorge Núñez Mar 17 '13 at 11:39
  • Yikes, I wasn't keen in assignments-as-expressions before, but that's just exacerbated things... – Louis Jackman Mar 17 '13 at 11:41

3 Answers3

3

Java runtime is far removed from your source code, and even from bytecode. Once a method is JIT-compiled, the resulting machine code is aggressively optimized. But more importantly, the details of this optimization are way beyond any specification. If it interests you, you may dive deep down into an implementation like HotSpot, but all you learn from it will be specific to the platform, version, build number, JVM startup arguments, and even individual run of the JVM.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
2

Once the code has been optimized (either by a C++ compiler or by a JIT), I would expect something similar to the following:

Source:

int x = 4, y = 8;
return f(++x) + y++;

Instructions:

PUSH 5
CALL f
POP INTO A
ADD 8 to A
RET A

The instructions I've listed are correct (unless I've made some silly error), and they only require code transformations that I know optimizers to be capable of making. Basically, unused results are not calculated and operations with known results are computed at optimization time.

It's quite possible that on a particular architecture there's a more efficient way to do it, though, in which case there's a good chance that the optimizer will know about it and use it. Optimizers often beat my expectations, since I'm not an expert assembly programmer. In this case, it's quite likely that the calling convention dictates the same location for the return value in the case of function f and this code. So there might be no need for any POP -- the return register/stack location can just have 8 added to it. Furthermore, if the function f is inlined then optimization will be applied after inlining.

So for example if f returns input * 2 then the whole function might optimize to:

RET 18
Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • Yes, a realistic instruction set has many finesses; it may turn out that there is a specifically optimized instruction that embeds a constant up to 127 into the opcode and it pays more to use that embedded constant and then increment a few more times to get the number, and so on. These days there is very little rewarding insight to gain at this level, unless you are in the business of implementing optimizing compilers. – Marko Topolnik Mar 17 '13 at 11:47
  • @MarkoTopolnik: indeed, when reading the disassembly you might think "hmm, that appears to be a somewhat roundabout way of adding 129, but I assume it's the most efficient", but as long as it's recognizably adding 129 you don't pay any attention. In more complex functions, optimizers find really clever ways to combine multiple operations, and it can get a bit more interesting. But as long as you trust the optimizer it doesn't usually *matter* what code it emits :-) – Steve Jessop Mar 17 '13 at 11:56
  • 1
    Here's a [nice talk by Guy Steele](http://www.infoq.com/presentations/Thinking-Parallel-Programming) where he goes into the minute details of hand-optimization that he and his generation of programmers used to do on a regular basis. Modern optimizers are not *that* clever, but I think the overall mess that results is comparable :) – Marko Topolnik Mar 17 '13 at 12:03
1

You can see exactly what the compiler generates using javap. For example:

int x = 4, y = 8;
return f(++x) + y++;

was compiled to this sequence of bytecode:

0:  iconst_4
1:  istore_1
2:  bipush  8
4:  istore_2
5:  aload_0
6:  iinc    1, 1
9:  iload_1
10: invokevirtual   #2; //Method f:(I)I
13: iload_2
14: iinc    2, 1
17: iadd
18: ireturn

Of course, it's up to the JVM how to turn this into assembler - see Disassemble Java JIT compiled native bytecode for how you can see the results in OpenJDK 7.

Community
  • 1
  • 1
Joe
  • 29,416
  • 12
  • 68
  • 88