Looking at this experimentally helps to see how you apply the math.
Software doesn't change math, the identities ,or other manipulations you do to simplify or change the equation using variables. Software performs the operations asked for. Fixed point does them perfectly unless you overflow.
And from math not from software we know that neither b or c can be zero for this to work. What software adds is that b*c can't be zero either. And if it overflows then the result is wrong.
a / b / c = (a/b) * (1/c) = (a*1) / (b*c) = a / (b*c)
from grade school and you can implement either a/b/c or a/(b*c) in your programming language: it is your choice. MOST of the time if you stick to integers the results are incorrect due to the fraction not being represented. A fair amount of the time the results is incorrect if you use floating point, for the same reason: not enough bits to store infinitely large or small numbers as is the case with the pure math. So where do you run into these limits? You can start to see this with a simple experiment.
Write a program that goes through all the possible combinations of numbers between 0 and 15 for a, b and c, compute a/b/c and a/(b*c) and compare them. Since these are four bit numbers remember the intermediate values cannot exceed 4 bits either if you want to see what a programming language is going to do with this. Printed only the divide by zeros and the ones where they don't match.
Immediately you will see that allowing any of the values to be zero makes for a very uninteresting experiment you either get a lot of divide by zeros or 0/something_not_zero isn't an interesting result.
1 2 8 : divide by zero
1 3 11 : 1 0
1 4 4 : divide by zero
1 4 8 : divide by zero
1 4 12 : divide by zero
1 5 13 : 1 0
1 6 8 : divide by zero
1 7 7 : 1 0
1 8 2 : divide by zero
1 8 4 : divide by zero
1 8 6 : divide by zero
1 8 8 : divide by zero
1 8 10 : divide by zero
1 8 12 : divide by zero
1 8 14 : divide by zero
1 9 9 : 1 0
1 10 8 : divide by zero
1 11 3 : 1 0
1 12 4 : divide by zero
1 12 8 : divide by zero
1 12 12 : divide by zero
1 13 5 : 1 0
1 14 8 : divide by zero
1 15 15 : 1 0
2 2 8 : divide by zero
2 2 9 : 1 0
2 3 6 : 1 0
2 3 11 : 2 0
So up to this point the answers match. For a small a or specifically a=1 that makes sense the result is either going to be 0 or 1. And both paths will get you there.
1 2 8 : divide by zero
At least for a=1, b=1. c=1 gives 1 the rest give a result of 0.
2*8 = 16 or 0x10 which is too many bits so it overflows and the result is 0x0 a divide by zero so that you have to look for no matter what, float or fixed point.
1 3 11 : 1 0
First interesting one:
1 / (3*11) = 1 / 0x21 which means 1/1 = 1;
1 / 3 = 0, 0 / 11 = 0.
so they don't match. 3*11 overflowed.
and this goes on.
So making ra a bigger number might make this more interesting? A small a variable is going to make the result 0 most of the time anyway.
15 2 8 : divide by zero
15 2 9 : 7 0
15 2 10 : 3 0
15 2 11 : 2 0
15 2 12 : 1 0
15 2 13 : 1 0
15 2 14 : 1 0
15 2 15 : 1 0
15 3 6 : 7 0
15 3 7 : 3 0
15 3 8 : 1 0
15 3 9 : 1 0
15 3 10 : 1 0
15 3 11 : 15 0
15 3 12 : 3 0
15 3 13 : 2 0
15 3 14 : 1 0
15 3 15 : 1 0
15 4 4 : divide by zero
15 4 5 : 3 0
15 4 6 : 1 0
15 4 7 : 1 0
15 4 8 : divide by zero
15 4 9 : 3 0
15 2 9 : 7 0
15 / (2 * 9) = 15 / 0x12 = 15 / 2 = 7.
15 / 2 = 7; 7 / 9 = 0;
15 3 10 : 1 0
15 3 11 : 15 0
both cases overflow not interesting.
So change your program to only show the ones where the result doesn't match but there is also not an overflow of b*c....no output. There is no magic or difference between doing this with 4 bit values vs 8 bit vs 128 bit....it just allows for you to have more results that can possibly work.
0xF * 0xF = 0xE1, and you can easily see this doing long multiplication in binary, worst case to cover all possible N bit values you need 2*N bits to store the result without an overflow. So backward for division an N bit numerator limited by a N/2 number of bits denominator can cover all the fixed point values of each with an N bit result. 0xFFFF / 0xFF = 0x101. 0xFFFF / 0x01 = 0xFFFF.
So if you want to do this math and can insure that none of the numbers exceeds N bits then if you do the math using N*2 number of bits. You wont have any multiply overflows, you still have divide by zeros to worry about.
To demonstrate this experimentally try all combinations from 0 to 15 of a,b,c but do the math with 8 bit variables instead of 4 (checking for divide by zero before every divide and tossing those combinations out) and the results always match.
So is there a "better"? Both multiply and divide can be implemented in a single clock using a ton of logic, or multiple clocks using exponentially less, despite your processor docs saying it is a single clock it may still be multiple as there are pipelines and they can hide a 2 or 4 cycle of either into some pipes and save a ton of chip real-estate. Or some processors don't do divide at all to save space. Some of the cortex-m cores from arm you can compile for single clock or multi-clock divide saving space only hurting when someone does a multiply (who does multiplies in their code??? or divides???). You will see when you do things like
x = x / 5;
depending on the target and the compiler and optimization settings that can/will get implemented as x = x * (1/5) plus some other movements to make that work.
unsigned int fun ( unsigned int x )
{
return(x/5);
}
0000000000000000 <fun>:
0: 89 f8 mov %edi,%eax
2: ba cd cc cc cc mov $0xcccccccd,%edx
7: f7 e2 mul %edx
9: 89 d0 mov %edx,%eax
b: c1 e8 02 shr $0x2,%eax
e: c3 retq
divide is available on that target as well as multiply but the multiply is perceived to be better, perhaps because of clocks, perhaps other.
so you may wish to take that into account.
If doing a/b/c you have to check for a divide by zero twice, but if doing a / (b+c) you only have to check once. The check for a divide by zero is more costly than the math itself for a 1 or near 1 number of clocks per ALU instruction. So the multiply ideally performs better, but there are possible exceptions to that.
You can repeat all of this using signed numbers. And the same holds true. If it works for 4 bits it will work for 8 and 16 and 32 and 64 and 128 and so on...
7 * 7 = 0x31
-8 * -8 = 0x40
7 * -8 = 0xC8
That should cover the extremes, so if you use twice the number of bits than your worst case, you don't overflow. You still have to check for divide by zero before each divide, so the multiply solution results in only one check for zero. If you double the number of bits you need then you don't have to check the multiply for an overflow.
There is no magic here, this is and was all solved with basic math. When do I overflow, using pencil and paper and no programming language (or a calculator as I did to make it faster) you can see when. You can also use more of that grade school math. the msbit of b for N bits is b[n-1] * 2^(n-1) right? with a 4 bit number, unsigned, the msbit would be 0x8 which is 1 * 2^(4-1); and that is true for the rest of b (b[3] * 2^3) + (b[2] * 2^2) + (b[1] * 2^1) + (b[0] * 2^0); Same for C so when we multiply those out using simple math we start with (b[3]c[3])(2^(3+3)), if you sit down and work that out your worst case cannot exceed 2^8. Can also look at it this way:
abcd
* 1111
=========
abcd
abcd
abcd
+ abcd
=========
xxxxxxx
7 bits plus the possibility of a carry that makes it a total of 8 bits. All simple simple math to see what the potential problems are.
Experiments will show no bit patterns that fail (divide by zero doesn't count that doesn't work for a/b/c = a/(b*c) either). John Coleman's answer to view it from another angle may help feel that all bit patterns will work. Although that was all positive numbers. This works for negative numbers as well so long as you check all your overflows.