Live data from Hacker News

Why is 2 * (i * i) faster than 2 * i * i in Java?

stackoverflow.com

101–109 of 109 posts

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#101
post #64

So it's an issue of the optimizer; as is often the case, it unrolls too aggressively and shoots itself in the foot, all the while missing out on various other opportunities. In my experience, loop unrolling should basically never be done except in extremely degenerate cases; I remember not long ago someone I know who also optimises Asm remarking "it should've died along with the RISC fad". The original goal was to re…

Yeah, sometimes the compiler unrolls too much and innocent looking one-liner can be compiled into a monstrosity like this: https://godbolt.org/z/aKtko5

Are there no compilers which attempt to look at that code, decide 'that looks like 1=2', and replace the code entirely?

There must be so many examples of bubble sort where quicksort would be better, and other code patterns which can be identified and replaced with something orders of magnitude faster.

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#102

Is Overflow UB so the compiler can choose to ignore the fact that 2x(i x i) could overflow differently from 2 x i x i? I’m not sure it does overflow differently but I would expect overflow to behave consistently as written, and not be dependent on optimization, is that not the case?

Nothing you can do in pure Java code is UB in the C/C++ sense.

Without UB it must be very hard for the compiler to optimize arithmetic. Even obvious things like (2 x A) x B vs 2 x (A x B) are only equivalent without overflow. I guess it can be specified as being up to the jitter to decide - so not UB but not known from looking at the source either? Would be interesting to know what .NET and Java specifications say on it

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#103

Earlier quoted context omitted.

Nothing you can do in pure Java code is UB in the C/C++ sense.

Without UB it must be very hard for the compiler to optimize arithmetic. Even obvious things like (2 x A) x B vs 2 x (A x B) are only equivalent without overflow. I guess it can be specified as being up to the jitter to decide - so not UB but not known from looking at the source either? Would be interesting to know what .NET and Java specifications say on it

You can usually optimize integer arithmetic just fine, including the example you gave (both forms are equivalent - try it!).

Floating point arithmetic is different, but Java gives itself wiggle room by not exactly specifying many results unless you choose "strict math". That's not UB though: it's just a range of possible outcomes.

Java can't have UB in the C/C++ sense, since it would break the security sandbox. It certainly has things without specifically defined values, such as hashCode() and what happens under data races isn't entirely deterministic, but it doesn't approach UB in the C/C++ sense.

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#104

Earlier quoted context omitted.

Without UB it must be very hard for the compiler to optimize arithmetic. Even obvious things like (2 x A) x B vs 2 x (A x B) are only equivalent without overflow. I guess it can be specified as being up to the jitter to decide - so not UB but not known from looking at the source either? Would be interesting to know what .NET and Java specifications say on it

You can usually optimize integer arithmetic just fine, including the example you gave (both forms are equivalent - try it!). Floating point arithmetic is different, but Java gives itself wiggle room by not exactly specifying many results unless you choose "strict math". That's not UB though: it's just a range of possible outcomes. Java can't have UB in the C/C++ sense, since it would break the security sandbox. It ce…

Yeah I’m painfully aware of the FP gotchas. But are you saying there are usually never any issues with integer arithmetic and overflow vs. optimizations (reordering, common subexpressions etc)? A branch like “if a+1 Seems that not checking for overflow and not being able to assume there is no overflow, would give the worst of both worlds (slower because of lack of some optimizations but still not safe against overflow like C#’s “checked”).

I thought a deref of a possibly overflown value was what could risk security, ie so long as all array indices and similar are range checked then nothing bad can happen?

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#105

Earlier quoted context omitted.

You can usually optimize integer arithmetic just fine, including the example you gave (both forms are equivalent - try it!). Floating point arithmetic is different, but Java gives itself wiggle room by not exactly specifying many results unless you choose "strict math". That's not UB though: it's just a range of possible outcomes. Java can't have UB in the C/C++ sense, since it would break the security sandbox. It ce…

Yeah I’m painfully aware of the FP gotchas. But are you saying there are usually never any issues with integer arithmetic and overflow vs. optimizations (reordering, common subexpressions etc)? A branch like “if a+1 Seems that not checking for overflow and not being able to assume there is no overflow, would give the worst of both worlds (slower because of lack of some optimizations but still not safe against overflo…

> But are you saying there are usually never any issues with integer arithmetic and overflow vs. optimizations (reordering, common subexpressions etc)?

I'm saying that it is uncommon. For example, the example you gave works fine! Most examples work fine since you can do most of the same type of transformations. The exceptions are largely operations where the upper bits affect the lower bits. Division is one, and right shift is another, but nearly all the other bitwise and math operations do not have this property.

The other place where signed-wrapping-is-UB is often used for optimization is in things like loop bounds. Given something like:

    for (int i = start; i 
Due to the UB of signed wrapping, the compiler can assume that initially i In Java, the compiler couldn't take advantage of that. However, the impact isn't as big since in any loop that accesses arrays based on the index, bounds checks have to be made, and a typical pattern is to do some up front bounds checks [1] which can guarantee than the main body of the loop can then be run without additional checks: and this subsumes the checks that would be needed due to wrapping signed values anyways. So basically you have to do those checks anyways in many cases.

> A branch like “if a+1 Sure, but these cases aren't very interesting for comparing the effectiveness of the signed overflow optimizations. It's a case where the optimization breaks the (wrongly expressed) intent of the programmer, so the difference is only between a fast, broken program and a slower, perhaps correct one.

Presumably in the signed-overflow-is-UB world, such a check will have to re-written in a different way (which might even end up slower).

> Seems that not checking for overflow and not being able to assume there is no overflow, would give the worst of both worlds (slower because of lack of some optimizations but still not safe against overflow like C#’s “checked”).

Not exactly - it's just a different point on the spectrum. On the one side you have overflow is UB, which leads to some (fairly limited) optimization opportunities, but also more unexpected results and broken programs, and on the far other size you have overflow is an always-caught checked error. Java is somewhere in the middle: overflow is well-defined (it wraps), which gets you most of the speed of the UB approach (no checking needed, only a few relatively minor optimizations are lost) without the unexpected results of UB - but you still have broken programs when overflow actually occurs (unless it was unexpected). See also Rust's strategy here which is interesting.

> I thought a deref of a possibly overflown value was what could risk security, ie so long as all array indices and similar are range checked then nothing bad can happen?

When "nothing bad can happen" from the point of view of the JVM, i.e., the security sandbox isn't broken, you can't access arbitrary memory, violate the type system, interfere with unrelated code, etc. Of course, overflowing an index and then accessing into the array could still do plenty of "bad things" depending on the higher level semantics of the program, since you are now executing an unexpected code path. You might return sensitive data to an attacker, whatever.

---

[1] More details: https://wiki.openjdk.java.net/display/HotSpot/RangeCheckElim...

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#106

Earlier quoted context omitted.

Okay, manual loop unrolling definitely helps, but the programmer MUST be aware of dependency chains and ILP. The compiler cannot make the decision, at least not without a pragma or maybe an autovectorization engine. At least, I haven't seen todays (2018) compilers cut dependency chains on without a #pragma omp reduce, or other assistance from the programmer. I've unrolled loops myself to good sucess. But it isn't as…

You are right, the vast majority of loops should be left without unrolling. In fact, the majority of code should be compiled for size, not for speed. For a typical application you probably have 99% of the code not being very performance sensitive, and the remaining 1% is where the time is spent. Still, people regularly compile for speed to good effect, because it's so important to compile that 1% for speed that you a…

I had to give it some thought.

Integer-based code probably can (theoretically) be optimized by a good enough compiler I haven't thought of all overflow / underflow conditions, but most "normal" code, consisting of adds, subtracts, and AND/OR/XORs, probably can be broken up as needed by the compiler.

But floating-point code MUST execute in precisely the order that the programmer specifies. In general, (A+B)+C does NOT equal A + (B+C) for floats.

For integer-code, divison is definitely order-specific, while multiplication might be order-specific in overflow conditions (I haven't 100% thought this through yet...). But in any case, the compiler can probably split up operations to take advantage of ILP as long as the results output the same bits.

Which isn't true for some operations (ie: floating point add)

--------

In any case, the "#pragma omp reduce" gives the compiler assurance from the programmer that the ordering does not matter. I think that's why something like that could benefit from parallelism a bit better.

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#107

Earlier quoted context omitted.

Yeah I’m painfully aware of the FP gotchas. But are you saying there are usually never any issues with integer arithmetic and overflow vs. optimizations (reordering, common subexpressions etc)? A branch like “if a+1 Seems that not checking for overflow and not being able to assume there is no overflow, would give the worst of both worlds (slower because of lack of some optimizations but still not safe against overflo…

> But are you saying there are usually never any issues with integer arithmetic and overflow vs. optimizations (reordering, common subexpressions etc)? I'm saying that it is uncommon. For example, the example you gave works fine! Most examples work fine since you can do most of the same type of transformations. The exceptions are largely operations where the upper bits affect the lower bits . Division is one, and rig…

Thanks that's very kind to take the time to write that much!

> leads to some (fairly limited) optimization opportunities,

Ok, I was just mistaken in my belief that integer overflow shenanigans was a major contributor to how a modern compiler optimized e.g. loops.

> you can't access arbitrary memory, violate the type system, interfere with unrelated code

Right. I was considering the sandbox in the sense only of process security rather than program/type soundness.

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#108

Earlier quoted context omitted.

> But are you saying there are usually never any issues with integer arithmetic and overflow vs. optimizations (reordering, common subexpressions etc)? I'm saying that it is uncommon. For example, the example you gave works fine! Most examples work fine since you can do most of the same type of transformations. The exceptions are largely operations where the upper bits affect the lower bits . Division is one, and rig…

Thanks that's very kind to take the time to write that much! > leads to some (fairly limited) optimization opportunities, Ok, I was just mistaken in my belief that integer overflow shenanigans was a major contributor to how a modern compiler optimized e.g. loops. > you can't access arbitrary memory, violate the type system, interfere with unrelated code Right. I was considering the sandbox in the sense only of proces…

> Ok, I was just mistaken in my belief that integer overflow shenanigans was a major contributor to how a modern compiler optimized e.g. loops.

Yes, there is some impact but I think it's large, at least based on looking at a lot of assembly, and going over the typical examples of where it helps. In the cases it does help, it could make a big difference on a loop, let's say 2x the speed, but these aren't all that common.

> Right. I was considering the sandbox in the sense only of process security rather than program/type soundness.

Usually those two things end up tightly bound together: it's hard to impossible to enforce a sandbox if the user can escape the type system.

Re: Why is 2 * (i * i) faster than 2 * i * i in Java?

#109
The graal behavior is a lot more sane:

    graal:
    [info] SoFlow.square_i_two   10000  avgt   10  5338.492 ± 36.624  ns/op   // 2 *\sum i * i
    [info] SoFlow.two_i_         10000  avgt   10  6421.343 ± 34.836  ns/op   // \sum 2 * i * i
    [info] SoFlow.two_square_i   10000  avgt   10  6367.139 ± 34.575  ns/op   // \sum 2 * (i * i)
    regular 1.8:
    [info] SoFlow.square_i_two   10000  avgt   10  6393.422 ± 27.679  ns/op
    [info] SoFlow.two_i_         10000  avgt   10  8870.908 ± 35.715  ns/op
    [info] SoFlow.two_square_i   10000  avgt   10  6221.205 ± 42.408  ns/op
The graal-generated assembly for the first two cases is nearly identical, featuring unrolled repetitions of sequences like

    [info]   0x000000011433ec03: mov    %r8d,%ecx
    [info]   0x000000011433ec06: shl    %ecx               ;*imul {reexecute=0 rethrow=0 return_oop=0}
    [info]                                                 ; - add.SoFlow::test_two_i_@15 (line 41)
    [info]   0x000000011433ec08: imul   %r8d,%ecx          ;*imul {reexecute=0 rethrow=0 return_oop=0}
    [info]                                                 ; - add.SoFlow::test_two_i_@17 (line 41)
    [info]   0x000000011433ec0c: add    %ecx,%r9d          ;*iadd {reexecute=0 rethrow=0 return_oop=0}
    [info]                                                 ; - add.SoFlow::test_two_i_@18 (line 41)
    [info]   0x000000011433ec0f: lea    0x5(%r11),%r8d     ;*iinc {reexecute=0 rethrow=0 return_oop=0}
    [info]                                                 ; - add.SoFlow::test_two_i_@20 (line 40)

while the third case does a single shl at the end.

    [info]   0x000000010e2918bb: imul   %r8d,%r8d          ;*imul {reexecute=0 rethrow=0 return_oop=0}
    [info]                                                 ; - add.SoFlow::test_square_i_two@15 (line 32)
    [info]   0x000000010e2918bf: add    %r8d,%ecx          ;*iadd {reexecute=0 rethrow=0 return_oop=0}
    [info]                                                 ; - add.SoFlow::test_square_i_two@16 (line 32)
    [info]   0x000000010e2918c2: lea    0x3(%r11),%r8d     ;*iinc {reexecute=0 rethrow=0 return_oop=0}
    [info]                                                 ; - add.SoFlow::test_square_i_two@18 (line 31)                                   
Both graal and C2 inline, but as usual the graal output is a lot more comprehensible.
Post reply on HN