3

In grade-school math with rational numbers, the expression (a / b) / c is equivalent to a / (b * c) by basic algebraic manipulation.

Is the same true when / is truncating integer division as in C and most other languages? That is, can I replace a series of divisions with a single division by the product of all the divisors?

You may assume that the multiplication does not overflow (if it does, it is obvious that they are not equivalent).

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • 2
    programming languages dont change the rules of math. – old_timer Jan 20 '19 at 19:59
  • 1
    @old_timer - I'm not sure what you mean. The rules of math depend on the semantics of the operations. The rules I learned at school generally applied to real or rational numbers and not to "truncating division" - so it is not entirely obvious which ones apply (certainly some don't, such as `a / b * b == a`). So you can interpret this question as about a specific type of math as implemented in C, if you wish. Consider also that most "math" rules don't apply at all to floating point math (which I'm not asking about here). – BeeOnRope Jan 20 '19 at 20:03
  • 1
    100 / 2 / 5 = 10. 100 / (2*5) = 10; so long as you dont have a fraction, it is true on paper and in a programming language. The one problem you have with math is a fraction if you limit to integers, but the fact that the division works it still works before you plug in the numbers – old_timer Jan 20 '19 at 20:06
  • 1
    [Apparently, it is equivalent](https://ideone.com/HQk2sQ) ... but don't accept this comment as *truth*. See John Coleman's answer. – pmg Jan 20 '19 at 20:06
  • the problem you have with computers is usually there is a limit to the size of the integers, so you have to be careful not to overflow. then you end up with a fraction at the end if the denominator of the division is larger. – old_timer Jan 20 '19 at 20:07
  • @old_timer - yes, it obviously works if there are no remainders. I'm interested, however, in the general case with remainders. – BeeOnRope Jan 20 '19 at 20:08
  • why wouldnt it? the remainder means fraction yes? certainly in C unless you compute the modulo as well... – old_timer Jan 20 '19 at 20:09
  • @pmg - yeah, I did the same thing, but yup I was hopeful for a _proof_ (ideone could give you a prof if you do it exhaustively for all numbers, but that might take ... a while). – BeeOnRope Jan 20 '19 at 20:09
  • the same proof that shows it works in math shows it works....in math.... – old_timer Jan 20 '19 at 20:10
  • @old_timer - because the remainder is discarded. It isn't immediately obvious to me that two separate divisions, discarding the remainder twice, is equivalent to a single division by the product, discarding the remainder once). – BeeOnRope Jan 20 '19 at 20:10
  • Im sure there is a math stackexchange if you are looking for a mathematical proof you are in the wrong place. – old_timer Jan 20 '19 at 20:11
  • it is immediately obvious, if the first division has a fraction, then the math wont work for integers, game over. it is easy to check for that in C or any other way that you can do math. – old_timer Jan 20 '19 at 20:13
  • by multiplying the numbers first you make the answer more accurate by not having two chances at a fraction. only one. – old_timer Jan 20 '19 at 20:13
  • but then you increase the chance of the multiply overflowing. this is trivial to do an experiment on, take 8 bit numbers for example try all the combinations both ways compare the results, then compare that with a floating point result. Program should take roughly 2 to 3 minutes to write. This question has been open for almost an hour. – old_timer Jan 20 '19 at 20:15
  • @old_timer - you've lost me at this point. If you aren't going to attempt an answer, maybe it is time to move on to a question that suits you better. – BeeOnRope Jan 20 '19 at 20:18
  • Aside from overflow and pathological cases such as `INT_MIN / -1 / -1` which has undefined behavior hence is not equivalent to `INT_MIN / 1`, I cannot think of a reason for `a / b / c` to differ from `a / (b * c)`. – chqrlie Jan 20 '19 at 20:27
  • you also need to worry about divide by zero – old_timer Jan 20 '19 at 20:34
  • 1
    @chqrlie - good point about `INT_MIN` and `-1` although at least in that case the UB is in the "right direction" - that is, the transformed version is defined while the original isn't. So the transformation is "safe" at least wrt that issue: after all if the original form was UB you can't really expect anything in particular of the transformed version (if you are interested in the reverse transformation it is a problem of course). – BeeOnRope Jan 20 '19 at 20:45

3 Answers3

6

The answer is "yes" (at least for nonnegative integers). This follows from the division algorithm which states that for any positive integers a,d we have a = dq+r for unique nonnegative integers q,r with 0 <= q <= d-1. In this case q is a/d in integer division.

In a/b/c (with integer division) we can think of it in two steps:

a = b*q_1 + r_1  // here q_1 = a/b and 0 <= r_1 <= b-1
q_1 = c*q_2 + r_2 // here q_2 = q_1/c = a/b/c and 0 <= r_2 <= c-1

But then

a = b*q_1 + r_1 = b*(c*q_2 + r_2) + r_1 = (b*c)*q_2 + b*r_2 + r1

Note that 0 <= b*r_2 + r_1 <= b*(c-1) + b-1 = bc - 1

From this it follows that q_2 is a/(b*c). Thus a/b/c = a/(b*c).

John Coleman
  • 51,337
  • 7
  • 54
  • 119
  • Thanks, it was the proof I was looking for. With negative numbers, assuming _consistent_ rounding towards 0 or rounding towards -infinity, does something similar hold? – BeeOnRope Jan 20 '19 at 20:16
0

Yes, on the integers. Someone already posted (and deleted?) an example of how it might not work on floating point. (Although, it might be close enough for your application.)

@JohnColeman has an argument from theory, but here's an argument from experiment. If you run this code:

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define LIMIT 100
#define NUM_LIMIT 2000

int main() {
    for(int div1 = -LIMIT; div1 < LIMIT; div1++) {
        for(int div2 = -LIMIT; div2 < LIMIT; div2++) {
            if(div1 == 0 || div2 == 0) continue;
            for(int numerator = -NUM_LIMIT; numerator < NUM_LIMIT; numerator++) {
                int a = (numerator / div1) / div2;
                int b = numerator / (div1 * div2);
                if(a != b) {
                    printf("%d %d\n", a, b);
                    exit(1);
                }
            }
        }
    }
    return 0;
}

It tries the division both ways, and reports an error if they are different. Running it in the following ways does not report an error:

  • div1, div2 from -5 to 5, and numerator from -INT_MAX/100 to INT_MAX/100
  • div1, div2 from -2000 to 2000, and numerator from -2000 to 2000

Therefore, I would bet that this will work with any integers. (Assuming, again, that div1*div2 does not overflow.)

Nick ODell
  • 15,465
  • 3
  • 32
  • 66
  • 2
    *I would bet that this will work with any integers* hardly qualifies as proof. – chqrlie Jan 20 '19 at 20:37
  • I also used the "try some numbers" approach, both in my head and with a program like the above. The problem is that I don't consider it _definitive_ unless you can do it exhaustively. Exhaustively is _perhaps_ feasible for 16-bit numbers (a few days to search the 48-bit space if you can get it down to 1 ns per check), but even with 32-bit numbers it isn't. Also, I guess C has impl-defined behavior for division, so you run the risk of having it work on your machine, but not always according to the standard. – BeeOnRope Jan 20 '19 at 20:42
-2

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.

halfer
  • 19,824
  • 17
  • 99
  • 186
old_timer
  • 69,149
  • 8
  • 89
  • 168
  • Note if you want to add rounding then that is pretty easy being base 2 you only need to make the number(s) twice the size (((a<<1)/(b*c)) +1)>>1; but now you have to watch for overflow again...or do you? – old_timer Jan 20 '19 at 22:00