The compiler should detect that the two expressions are strictly equivalent and generate whatever code it believes is the fastest.
Any idea why it is this way?
The compiler should detect that the two expressions are strictly equivalent and generate whatever code it believes is the fastest.
Any idea why it is this way?
fn main() {
let a: i8 = 125;
let b: i8 = 3;
let c: i8 = (a + b) / 2;
let d: i8 = b + ((a - b) / 2);
println!("{} {}", c, d);
}
This program outputs `-64 64` although the computations of `c` and `d` are equivalent.Here's another example using floating point numbers:
fn main() {
let mut total1: f32 = 0.0;
let mut total2: f32 = 0.0;
let mut counter1: f32 = 0.0;
let mut counter2: f32 = 100.0;
for _ in 0 .. 10001 {
total1 += counter1;
total2 += counter2;
counter1 += 0.01;
counter2 -= 0.01;
}
println!("{} {}", total1, total2);
}
The output of this program is `500041.16 500012.16`, a difference of 25 for a program that computes the same result (unless I made a mistake).Whereas for int ops, equality works within the limits of the design.
In short equality means something different in fp by design. For int, it means what we think it means within its limits. When we overflow, then things get screwy.
However, the compiler ('s optimization step) is not magic and produces suboptimal code sometimes. Back when C was young, this was frequently the case (1970's and 1980's), so dropping into assembly to hand-code performance critical sections is just what people did, in order to get software to run smoothly.
Thankfully this is largely no longer the case, however it does still happen.
In Java's case, the JVM runs on top of multiple different architectures which makes optimization even more complicated.
Low-level instruction generation and optimization is just one topic under the umbrella of compiler design, which is a huge (and fascinating!) discipline to get into.
I agree that compilers are not always perfect, but in this particular case the two expressions are trivially equivalent from the associativity of the multiplication so the distinction had to be intentional.
But as gnuvince pointed out, the two expressions are not equivalent when you consider integer overflow.
<smallest possible positive number> * 0.5 * 2
When evaluated like that:
(<SPPN> * 0.5) * 2
the result is 0 * 2 = 0
And when evaluated the other way around:
<SPPN> * (0.5 * 2)
the result is <SPPN> * 1 = <SPPN>
double sppn = Double.MIN_VALUE;
double first = sppn * (0.5 * 2);
double second = (sppn * 0.5) * 2;
System.out.println(sppn);
System.out.println(first);
System.out.println(second);"An overview of the PL.8 compiler"
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.453...
Notice the architecture, quite similar to the layers and compiler phases used in modern compiler toolchains like LLVM.
The secret sauce, if one can call it as such, was that PL.8 had a richer type system, and the System/370 was a bit beefier than most platforms adopting C compilers.
Also what good will it bring? What if the canonical expression triggers the slow path? Now you have no means to change it into the fast version.
Further, in the case of floating point operations, operation order matters for rounding. And with integer operations, the actual form used can be important for preventing overflow (of intermediate results).