Which is better on Android: divide by 2 or shift by 1?
jakewharton.com
jakewharton.com
Instead, you can also see the array index as a bit string, where every bit tells you which path to go down, left or right. In that case, “shifting right by one bit” moves you up to the parent. “Shifting left” moves you down to the left child. Flipping one bit flips you over to the other child. Bit wise operations indeed seem more natural with that interpretation.
A lot of “power of 2” multiplication/division has similar interpretations. For example, when walking page tables, you could see walking down the levels as “dividing by the size of the granule”, or simply as “shifting right to select the index on that level”.
No contest on anything where the power of 2 is coincidence, i.e. for non “computery” things where there is no such underlying structure.
Bit of hypocrisy there, since degrees are literally the same thing, just with 360 sectors rather than (say) 256. Though the extra factors of 3 and 5 are sometimes useful.
I think this is another good example where a binary representation (and a shift) can be a more natural way to approach the problem.
As a quick example, if you're writing a numerical approximation or even just a LUT for sin(x), you can get away with only approximating/storing values for a reduced range by using some bit twiddling. The top bit becomes sign of output if you XOR it out first, the next bit lets you take advantage of sin(pi/2-x) = sin(x+pi/2) to half the input range again, eek out symmetry for the next bit and you can nearly approximate sin(x) as a linear function for some use cases.
I have had to puzzle over far too much code doing adds/subtracts/multiplies/divides instead of and/or/xor/shift FAR too often.
I blame Java not having an unsigned type. There are apparently some weird tricks you can do with arithmetic in Java that operate on things like an unsigned type without having to go up to the next higher integer width.
> One of the little experiments I tried was asking people about the rules for unsigned arithmetic in C. It turns out nobody understands how unsigned arithmetic in C works. There are a few obvious things that people understand, but many people don't understand it.
https://www.artima.com/intv/gosling3.html
Since 2014 that Java now has support for unsigned math via methods on its boxed types since Java 8.
And depending on how Valhalla goes, it might gain inline classes for unsigned types, which is pretty much the same thing as having an uint.
Since shifts are fundamentally simpler than multiplies it always makes sense to do this transform. This is one of a number of transforms that are generally called "strength reductions" <https://en.wikipedia.org/wiki/Strength_reduction>, converting for a more general expensive operation into a more constrained cheaper operation. In this case it is the equivalent to knowing that if you want to multiply a number by 10 you can just add a 0 at the beginning instead of having to write all the work by hand.
The only reason not to do this transform would be if you had a CPU that literally does not have a shift operation, but I cannot think of any such part. Even if you did have such a part, the odds are you could emulate a shift using other other instructions and still outperform the multiply.
This has been a standard optimization for half a century. The original C compiler for the PDP-11 did these transforms even when you turned off optimizations <http://c-faq.com/misc/shifts.html>.
Most processors will decode an instruction into micro-ops and guess what that decode phase can do? It can say "Is this a power of 2? Great, engage the shifting operator".
Only on super simple processors (think embedded systems) would this ever actually make a difference. Anything else, this is an optimization that your processor is going to do for you automatically.
Consider this, a common easily applied optimization that compilers have been doing for half a century MAY have made it's way into modern CPUs.
Transistors aren't nearly as power hungry as you paint them and CPUs aren't nearly as bad at optimization. There is no reason to switch a multiply or divide for a shift. The ONLY reason to make that switch is if you are dealing with the simplest of processors (Such as a microwave processors). If you are using anything developed in the last 10 years that consumes more than 1W of power, chances are really high that the you aren't saving any power by using shifts instead of multiples. It is the sort of micro-optimization that fundamentally misunderstands how modern CPUs actually work and over estimates how much power or space transistors actually need.
You are correct, that on modern CPUs there are often specifically recognized idioms where the processor can implicitly perform an instruction transform such as a strength reduction from a multiply to a shift.
Having said that, it still makes sense a compiler to perform strength reductions rather than depending on the CPU frontend, at least if your compiler has a relatively decent scheduling model for the CPU. I don't know of any modern production quality compiler that would omit a simple strength reduction like this and leave it to the CPU.
Multiply: Latency 3, Throughput 1 Shift: Latency 1, Throughput 2
If the ALU contained an early out or fast path for simpler multiplies, the latency would read 1-3. You can verify this by looking at div, which does early out and has a latency of 35-88.
Any compiler that doesn't swap a multiply to a shift when it can is negligent.
... well... If you have hardware multiply that is guaranteed to take one click cycle and a shift that will take the same... does fundamental complexity of how that happens even matter?
So they could do "x = (x + 1) & (limit - 1)". No division and branch free.
Can anyone elaborate what does benchmark=3/4 ns mean, and the count? Is the set-up part of the benchmark (test structure suggest not, but just to make sure)?
The only way I can read it is that 4000 divisions takes 4ns, and 4000 shift-rights takes 3ns, but that only has 1 digit of precision, which makes it unusable for comparison, but even then suggests a 25%/33% difference, which is not insignificant.
Also, the VM seems to optimise multiply out, so it must be doing it for a reason.
It stands to reason that if two operations take the same amount of time, but one requires more transistors to compute, power usage should diverge.
https://pvk.ca/Blog/2012/07/30/binary-search-is-a-pathologic...
There is no difference... because of the conversion.
Even ignoring those caveats, several commentators seem to have got the impression that this applies to the CPU.
Unless you have evidence otherwise (ASM differences or benchmarks), there is no use in manually transforming your arithmetic into something more complex but faster. The compiler will do it for you.
I'd certainly tend to agree with you in general but more for the reason that the compiler can abstract over hardware changes across time. I'd take that benefit over the risk of the optimisation not being applied for most code I write - non-optimisations would be considered bugs and probably/eventually fixed.
I'd strongly disagree the code is more complex (in this case).
It seems to me that they are equally simple if we assume that programmers dealing with low level or performance intensive code know what a bitwise shift is and ignore the extra character, then they are literally equivalently complicated expressions with 1 symbol and 1 value applied to the symbol.
Yes, the point of the article is to convince you to stop micro-optimizing code for imagined performance benefits, and instead code for readability (the compiler will worry about optimizing).
(I know nothing about hardware, it just intuitively seems like moving a bunch of bits over by 1 should be faster than dealing with xor and carries)
In this case I imagine you're right. Although also worth pointing out that due to the way modern CPUs are basically frontends to generate uOps that it could actually perform the optimization by itself anyway. Time to break out PAPI (very cool tool for anyone unaware, you can get instruction level profiling in your program with basically 4 function calls and a header file).
To put this in more concrete terms: an N-bit adder involves N 1-bit stages to add each bit, and then a 1-bit carry network on top of that, which has N stages in it. So overall, it's O(N) in terms of hardware. An N-bit shift unit is going to use lg N N-bit muxes--or O(N lg N) in terms of hardware. Total gate delay in both cases is O(lg N), but adders have O(N) hardware (and thus energy consumption) while shifters have O(N lg N).
A secondary consequence of being larger area is that a superscalar architecture may choose to have one execution unit that has an adder and a shifter and a second that only has the adder. So an addition may schedule better than a shift, since there are more things it can execute on.
O(N) adders cannot meet the latency demands of modern high-frequency CPUs. The actual complexity of adders in real CPUs is usually O(N²).
Yes, a fixed shift-by-one unit would be much simpler than an adder. But many (most?) CPUs that supports shifting have generic shift units, where the number of bits to shift varies, and that makes them much more complex.
The 'shift' vs 'multiply' (or add to self) for doubling came about because in the past, it was very common that shifts were faster than multiplies (if your CPU even had a multiply instruction) and often faster than adds as well.
Example, from the 8086 (yes, very long time ago, but this is the environment where the differences often massively mattered):
https://www.oocities.org/mc_introtocomputers/Instruction_Tim...
Add reg->reg: 3 clock cycles Mul: 70-133 depending on 8 vs. 16 bit size Shift: Reg with shift of 1 (which is a *2): 2 clock cycles.
Now, for divide by 2 the issue is even larger (as you can't 'subtract from itself' to achieve divide by 2):
Idiv: 101-184 clocks, depending on 8 vs. 16 bit size Shift: 2 clock cycles.
So, on the 8086, for times 2, a shift was 33% faster than an add to self (and so much faster than a Mul that no one should use Mul for times 2).
And for divide by 2, a shift was massively faster than an Idiv (2 cycles vs minimum of 101 cycles).
Now, these relative values change as one moves up the x86 CPU line to newer CPU's. Intel built faster adders, faster multipliers, faster dividers, so one really has to check the specific CPU to see which instruction is faster. But the one item that will remain fairly constant is that presuming that using a shift for powers of two multiply or divide is generally close to the 'fastest' method is a good ball-park estimate that is more often right than it is wrong.
Interesting info on 8086. Another approach that doesn't apply to OP's article, but does to x86 assembly is (ab)using LEA for small multiplications. At 2 clock cycles it looks competitive with shift for doubling, but can also be used for multiples like 3 and 5.
Intell added the MULX instruction that is similar to MUL, except that it doesn't touch the flags. It's very useful in bignum math.
Using shifts for constant divide has been a compiler code generator optimization for decades. This is not something programmers have needed to worry about in source code for a long time, unless targeting some small microcontroller that lacks fast divide hardware.
ART, which replaced Dalvik on 5.0 (available as experimental on 4.4), was AOT only up to version 7.0.
As it was proven that Android users lack the patience of a C++ developer when updating their apps, Google adopted another approach with version 7.0.
A multi-tier compiler infrastructure, composed by a very fast interpreter hand written in Assembly for fast startup, a JIT compiler for the first optimization level, with gathering of PGO data, then the AOT compiler runs in the background and when the device is idle gets that PGO data and just like a C++ compiler with PGO data, outputs a clean AOT compiled binary for the usual user workflow.
In case of an update or changes in the workflow that trigger the execution of code that wasn't AOT compiled, the process restarts.
As means to reduce this kind of de-optimizations, since Android 10 those PGO files are uploaded into the Play Store and when a user installs an application that already has PGO data available, it is downloaded alongside the APK and the AOT compiler can do its job right from the start.
In any case, he used dex2aot which is the AOT compiler daemon on Android.
Microsoft has gone through similar process with .NET for UWP, with the main difference that the AOT compiler lives on the Microsoft store and what gets downloaded is already straight binary code.
Apparently mixing language capabilities with toolchains keeps being an issue.
AFAIK there's no fused add-shift op that could be used.
To replace a signed division with ashr, you have to know that for all negative inputs, the value of the bits shifted out are all 0.
Write what you want to do, not how to do it. There is no difference.
The PGO metadata files also get shared across devices via the Play Store as means to steer the AOT compiler into the optimal level of optimization across all users of the application.
I assume that at the current level of ongoing ART optimizations, the team would consider that a compiler bug.
What would you recommend for an IDE? I used Eclipse some years ago. Is that still common?
I may want to experiment with some Android flavored Java again.
We are in 2020. Don't shift by 1 instead of /2 if you mean to /2.
Other random example: in some edge cases, integer division replacement by a multiplication can still be relevant today (depends on if its a constant, the compiler, and if nothing optimized also on the exact processor, though, because last models are already ultra-fast with the real integer divide instructions), but I suspect in 15 years (maybe even 10) this will be completely irrelevant, at least for high perf targets.
1. Looking at the Java bytecode is practically meaningless, you would have to look at the machine code the JIT is creating.
2. A division by 2 is identical to a shift right by 1 only if the integer is unsigned. Java integers are signed. Try this program in C to see for yourself:
int foo(int a) { return a/2; } int bar(int a) { return a>>1; }
Run gcc -S -O2 to get assembly output in text form.
Basically, the problem is this:
5/2 -> 2 (ok, rounds down)
5>>1 -> 2 (ok, same)
-5/2 -> -2 (ok, rounds down)
-5>>1 -> -3 (oops!)
3. The question is really about the JIT backend for the target platform, which means CPU platform, not OS platform. So "on Android" does not make much sense here, as Android exists for x86 and ARM and those JIT backends might behave differently.
-5/2 -> -2 (ok, rounds down)
Actually this is rounding up, to zero, as -2.5 < -2 < 0You can't just divide by 2 and expect that to be equivalent to shifting right by 1. That only works if the integer is unsigned. For signed integers, that won't yield the right result. This is trivial to verify.
He literally presents x86 and ARM assembly dumps where shift right generates one instruction, and divide generates that same instruction plus several others.
Then, he feels the need to run an unnecessary benchmark (most likely screwing it up somehow) and concludes there is no difference!
But how can there possibly be no performance difference, in general, between the CPU running an ALU instruction and running that same instruction plus several other ALU instructions?!?
It's almost unbelievable.
As to how he screwed up the benchmark, my guesses are that either he failed to inline the function (and the CPU is really bad), or failed to prevent the optimizer from optimizing the whole loop, or didn't run enough iterations, or perhaps he ran the benchmark on a different VM than what produced the assembly (or maybe somehow the CPU can extract instruction level parallelism in this microbenchmark, but obviously that doesn't generalize to arbitrary code).
OK, the "adds" is probably slower, and introduces a register dependency. But the whole exercise here is to test and see, not trust that some simple operation performs like how it looks.
Indeed, if you discount the "bx lr" (that's just the return, right?), the 1-instruction version takes 3ns, and the 4-instruction version takes 4ns. Clearly, 4 times as many instructions is not taking anywhere near 4 times as long to execute.