You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On my machine with primitives, these takes exactly the same amount of time and we can do a billion of them in a second. Is this more important with autodiff variables for some reason?
Two things. (1) Yes, more important with autodiff. Follow the chain rule---the expression for the derivatives involves more operations.
(2) I used the following evaluation code to make sure the compiler doesn't optimize away anything. Multiplication is way faster. What surprised me is that addition took about the same time as multiplication. The loop arithmetic's all basically free since the CPU has a separate pipeline for integers, though there is some overhead from the ongoing additions.
#include <iostream>
int main() {
double a;
for (double x = 1; x < 100000; ++x)
for (double y = 1; y < 100000; ++y)
a += x / y;
std::cout << "a = " << a << std::endl;
}
Division:
~/temp2$ clang++ -O3 race.cpp
~/temp2$ time ./a.out
a = 6.04501e+10
real 0m45.319s
user 0m45.218s
sys 0m0.069s
Multiplication:
~/temp2$ time ./a.out
a = 2.49995e+19
real 0m9.608s
user 0m9.593s
sys 0m0.010s
#include<stdlib.h>intmain (int argc, char* argv[]) {
double a = atof(argv[0]);
double b = atof(argv[1]);
double c = atof(argv[2]);
double d = 0;
for (int i = 0; i < 1000000000; ++i) {
//d += a / b / c;
d += a / (b * c);
}
return d;
}
and it was the same time to two decimal places with -O3. I also checked with a disassembler to make sure that there were two division instructions vs one division and one multiplication. My theory here is that you can actually pipeline two divisions at once for the same cost as one division and one multiplication? Pipelining throws a lot of microbenchmark results out the window in non-obvious ways.
I'll work out the chain rule stuff on paper when I get home tonight.
An issue with this
```
for (int i = 0; i < 1000000000; ++i) {
//d += a / b / c;
d += a / (b * c);
}
```
is that the compiler could recognize a / (b * c) is a constant
expression across the loop and pull it out. I think the clang++ on my
computer was doing this.
On 8/10/17, seantalts ***@***.***> wrote:
I tested this as well with the following code:
```cpp
#include <stdlib.h>
int main (int argc, char* argv[]) {
double a = atof(argv[0]);
double b = atof(argv[1]);
double c = atof(argv[2]);
double d = 0;
for (int i = 0; i < 1000000000; ++i) {
//d += a / b / c;
d += a / (b * c);
}
return d;
}
```
and it was the same time to two decimal places with -O3. I also checked with
a disassembler to make sure that there were two division instructions vs one
division and one multiplication. My theory here is that you can actually
pipeline two divisions at once for the same cost as one division and one
multiplication? Pipelining throws a lot of microbenchmark results out the
window in non-obvious ways.
I'll work out the chain rule stuff on paper when I get home tonight.
--
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
#596 (comment)
@bbbales2 hit the nail on the head here. You'll see that I used the loop variables and saved the result for printing at the end so the whole thing couldn't be compiled away.
How embarrassing, I must have been looking at the -O0 disassembly - the O3 does in fact do loop-invariant code motion. I feel like so often we have to do little tricks that it seems like the compiler should be able to optimize (like the one for this ticket) that I'd lost some of the respect due to the compiler's optimizer.
I converted Bob's code to a test of the original question (two divisions vs one division and one multiplication) here:
#include <iostream>
int main() {
double a = 0.0;
for (double x = 1; x < 1000; ++x)
for (double y = 1; y < 1000; ++y)
for (double z = 1000; z > 1; --z)
//a += x / y / z;
a += x / (y * z);
std::cout << "a = " << a << std::endl;
}
Do you guys see anything wrong with that?
It's also the same speed at -O3 for me. In the assembly, I actually see the proposed enhancement as having 3 additional instructions (all mulsds) and retaining the divisions, for some reason.
So I think there's a little weirdness going on in there due to the compiler deciding loop variables should be integers and then converting them to doubles every iteration. So I just took my original test and added volatile keywords, which tell the compiler that the variables could change at any time and to basically avoid optimizing those variables:
#include<stdlib.h>intmain (int argc, char* argv[]) {
volatiledouble a = atof(argv[0]);
volatiledouble b = atof(argv[1]);
volatiledouble c = atof(argv[2]);
volatiledouble d = 0;
for (int i = 0; i < 1000000000; ++i) {
//d += a / b / c;
d += a / (b * c);
}
return d;
}
I still see the same times observed for each version but I have simpler assembly for us to look at. Something weird is still going on in the (b*c) version as it again has 2 multiplications and 2 divisions for some reason.
Here's the a / b / c loop
This code might be tricky to read cause I think it's unrolled a little.
```
00000001000009bb add edx, 0xfffffffd
```
That is incrementing the loop by -3 maybe? At least that's what I saw
when I looked at the assembly produced by Clang.
Maybe just look at the assembly with -O1 to avoid the super duper optimizations.
Also:
```
for (double z = 1000; z > 1; --z)
a += x / y / z;
```
For this loop ```x / y``` is getting computed and cached outside of
the z loop for me haha, which would make the results confusing still.
Might be worth just taking the assembly and directly modifying it to
test the ops you're curious about. What Bob is saying though should be
true. Division is relatively hard (maybe 4-5x slower? -- this seems
right: https://stackoverflow.com/questions/39095993/does-each-floating-point-operation-take-the-same-time/39135689#39135689)
than multiplication and addition which usually cost about the same.
Neat. I'm surprised the code for the multiplication in the denominator is so much worse. It winds up doing just as many divisions, which I really don't understand.
@bbbales2 I know that division is slower than multiplication, I would just be surprised that the compiler writers wouldn't know that, you know? It's a very easy peephole optimization, easier even than the loop-invariant code motion we saw above. That said, I need to reason through the autodiff situation... seems like it's probably still a useful optimization in that case. I just want to make sure that when we undertake work like this that we understand how valuable it is and when it's applicable.
Also interesting - the (b *c) would seem to be less numerically stable (more likely to overflow)? I don't have a good grasp of that. And this points out that the way intermediate calculations are represented as doubles could affect the calculation differently, though it's not clear that matters to us.
I would just be surprised that the compiler writers wouldn't know that, you know? It's a very easy peephole optimization, easier even than the loop-invariant code motion we saw above.
Haha, that's a good point. Hadn't thought of it like that.
Guess it all comes down to the autodiff? The compiler isn't gonna be
able to do the optimizations with stan::var s, and the adjoints are
going to be slightly more annoying with the division.
@seantalts: Yes, we definitely want to understand what the compiler's going to do. The problem with that narrow strategy is that all the compilers do it slightly differently, so it's a moving target. So, as @bbbales2 says, we really should be doing these tests with autodiff types.
@seantalts Alternatively, a / b might overflow when a / (b * c) does not. And of course b * c can underflow and round down to zero when the division might not. So they will break down slightly differently.
Activity
seantalts commentedon Aug 10, 2017
On my machine with primitives, these takes exactly the same amount of time and we can do a billion of them in a second. Is this more important with autodiff variables for some reason?
bob-carpenter commentedon Aug 10, 2017
Two things. (1) Yes, more important with autodiff. Follow the chain rule---the expression for the derivatives involves more operations.
(2) I used the following evaluation code to make sure the compiler doesn't optimize away anything. Multiplication is way faster. What surprised me is that addition took about the same time as multiplication. The loop arithmetic's all basically free since the CPU has a separate pipeline for integers, though there is some overhead from the ongoing additions.
Division:
Multiplication:
bob-carpenter commentedon Aug 10, 2017
I didn't test
a / b / c---that might get unfolded intoa / (b * c). They're not quite equivalent w.r.t. overflow behavior at the size limits.seantalts commentedon Aug 10, 2017
I tested this as well with the following code:
and it was the same time to two decimal places with -O3. I also checked with a disassembler to make sure that there were two division instructions vs one division and one multiplication. My theory here is that you can actually pipeline two divisions at once for the same cost as one division and one multiplication? Pipelining throws a lot of microbenchmark results out the window in non-obvious ways.
I'll work out the chain rule stuff on paper when I get home tonight.
bbbales2 commentedon Aug 11, 2017
bob-carpenter commentedon Aug 11, 2017
@bbbales2 hit the nail on the head here. You'll see that I used the loop variables and saved the result for printing at the end so the whole thing couldn't be compiled away.
seantalts commentedon Aug 11, 2017
How embarrassing, I must have been looking at the -O0 disassembly - the O3 does in fact do loop-invariant code motion. I feel like so often we have to do little tricks that it seems like the compiler should be able to optimize (like the one for this ticket) that I'd lost some of the respect due to the compiler's optimizer.
I converted Bob's code to a test of the original question (two divisions vs one division and one multiplication) here:
Do you guys see anything wrong with that?
It's also the same speed at -O3 for me. In the assembly, I actually see the proposed enhancement as having 3 additional instructions (all
mulsds) and retaining the divisions, for some reason.Here's the loop for double division:
And here's the loop with the proposed enhancement (b*c):
seantalts commentedon Aug 11, 2017
So I think there's a little weirdness going on in there due to the compiler deciding loop variables should be integers and then converting them to doubles every iteration. So I just took my original test and added
volatilekeywords, which tell the compiler that the variables could change at any time and to basically avoid optimizing those variables:I still see the same times observed for each version but I have simpler assembly for us to look at. Something weird is still going on in the (b*c) version as it again has 2 multiplications and 2 divisions for some reason.
Here's the
a / b / cloopHere's the
a / (b * c)loop:bbbales2 commentedon Aug 11, 2017
bob-carpenter commentedon Aug 11, 2017
seantalts commentedon Aug 11, 2017
@bbbales2 I know that division is slower than multiplication, I would just be surprised that the compiler writers wouldn't know that, you know? It's a very easy peephole optimization, easier even than the loop-invariant code motion we saw above. That said, I need to reason through the autodiff situation... seems like it's probably still a useful optimization in that case. I just want to make sure that when we undertake work like this that we understand how valuable it is and when it's applicable.
seantalts commentedon Aug 11, 2017
Also interesting - the
(b *c)would seem to be less numerically stable (more likely to overflow)? I don't have a good grasp of that. And this points out that the way intermediate calculations are represented as doubles could affect the calculation differently, though it's not clear that matters to us.bbbales2 commentedon Aug 11, 2017
bob-carpenter commentedon Aug 11, 2017
@seantalts: Yes, we definitely want to understand what the compiler's going to do. The problem with that narrow strategy is that all the compilers do it slightly differently, so it's a moving target. So, as @bbbales2 says, we really should be doing these tests with autodiff types.
@seantalts Alternatively,
a / bmight overflow whena / (b * c)does not. And of courseb * ccan underflow and round down to zero when the division might not. So they will break down slightly differently.bob-carpenter commentedon Aug 11, 2017
For example, the loop-invariant code motion won't be legal with autodiff.
SteveBronder commentedon Apr 29, 2022
closing because its good advice but very general