Opened 21 years ago
Closed 17 years ago
#62 closed Bugs (Rejected)
Rational sum not optimal for overflows
| Reported by: | nobody | Owned by: | Jonathan Turkanis |
|---|---|---|---|
| Milestone: | Component: | None | |
| Version: | None | Severity: | |
| Keywords: | Cc: |
Description
The sum of n1/d1 + n2/d2 should be calculated as:
L*((n1'*d2'+n2'*d1')/m)
-----------------------
(k/m)*d1'*d2'
where
m = gcd(k, n1'*d2'+n2'*d1')
k = gcd(d1, d2)
L = gcd(n1, n2)
k*d1'=d1
k*d2'=d2
L*n1'=n1
L*n2'=n2
This way overflows are avoided while calculatins the
resulting numerator (since division by m is performed
before multiplication by L).
Manuel.Sequeira@iscte.pt
Change History (5)
comment:2 by , 21 years ago
Logged In: NO
The existing code fails if a rational is added to itself (r
+= r):
IntType g = gcd(den, r.den);
den /= g;
num = num * (r.den / g) + r.num * den;
^^^^^ Opps... value changed!
g = gcd(num, g);
num /= g;
den *= r.den / g;
^^^^^ Opps... value changed!
This requires a slight change:
IntType g = gcd(den, r.den);
// Must deal with r += r:
IntType rd = r.den;
den /= g;
num = num * (rd / g) + r.num * den;
g = gcd(num, g);
num /= g;
den *= rd / g;
This code requires 2 gcd(), 4 divisions and 3
multiplications.
I propose the following alternative:
IntType zero(0);
if(r.num == zero)
return *this;
IntType gn = gcd(num, r.num);
IntType g = gcd(den, r.den);
// Must deal with r += r:
IntType rd = r.den;
den /= g;
num = num / gn * (rd / g) + r.num / gn * den;
g = gcd(num, g);
num = gn * (num / g);
den *= rd / g;
The "if" is necessary in order to prevent calling gcd with
both arguments zero (alternatively gcd might be extended to
deal with that case by returning 1).
This code requires 3 gcd() (one more), 6 divisions (two
more) and 4 multiplications (one more)
The existing code overflows more easily than the proposed
one. Suppose *this is 13/21 and r is -13/66 (of course one
may find similar examples with larger numbers).
// num = 13, den = 21, r.num = -13, r.den = 66
IntType g = gcd(den, r.den); // g = 3
// Must deal with r += r:
IntType rd = r.den; // rd = 66
den /= g; // den = 7
num = num * (rd / g) + r.num * den; // num = 13 * (66 / 3) -
13 * 7 = 13 * 15 = 195
g = gcd(num, g); // g = gcd(195, 3) = 3
num /= g; // num = 65
den *= rd / g; // den = 7 * 66 / 3 = 154
Compare with what happens in the alternative:
// num = 13, den = 21, r.num = -13, r.den = 66
IntType zero(0);
if(r.num == zero)
return *this;
IntType gn = gcd(num, r.num); // gn = 13
IntType g = gcd(den, r.den); // g = 3
// Must deal with r += r:
IntType rd = r.den; // rd = 66
den /= g; // den = 7
num = num / gn * (rd / g) + r.num / gn * den; // num =
13/13 * (66/3) - 13/13 * 7 = 15
g = gcd(num, g); // g = gdc(15, 3) = 3
num = gn * (num / g); // num = 13 * (15 / 3) = 13 * 5 = 65
den *= rd / g; // den = 7 * 66 / 3 = 154
Notice that num reaches 195 in the original code but
remains at 15 in the version I propose.
Manuel.Sequeira@iscte.pt
comment:3 by , 21 years ago
Logged In: NO (SF isn't letting me log in. Grr.) First, the latest version (in CVS) has the self-assignment bug in += (and the other assignment operators) fixed. Thanks for pointing it out. As to the overflow issue, I'm confused. Your example of (13/21) + (-13/66) results in 65/154 in both versions, which is correct. Can you provide an example of a calculation which fails using rational<int> in the current version, and succeeds with your version. I expect it to require unusually complicated fractions (ie, very large numerators and/or denominators). Your version does 1 extra GCD, 2 extra divisions, and 1 extra multiplication. This could be a serious overhead for the key case, rational<LongInt> where LongInt is a user- defined unlimited-precision integer type. In this case, overflow cannot happen. On the other hand, working with bigger numbers *could* be more expensive (but I doubt that it would be significantly so...) In the case of rational<int>, the operations are cheap, and overflow *is* possible. But dealing with the issues of using rationals based on a limited-precision integer type are complex, and less well-understood than the related issues with floating point. Frankly, anyone using rational<int> for numbers which get even close to the overflow point, needs to know what they are doing. And for that sort of person, boost::rational isn't meant to be appropriate. So overall, I'm not too keen on this change. It hurts the case I care about, and doesn't add a lot to the (arguably more common, but nevertheless dangerous) case of rational<int>. Unless you come up with some more persuasive arguments, I'm not likely to change the library. I may be willing to make the documentation (section 9, "Design Notes", "Limited-range integer types") stronger regarding the danger, but I don't want to make it seem like scaremongering... Please feel free to provide further arguments, but make sure you understand the design goals (unlimited-precision integer is key, limited-precision is likely to be common, but will never be 100% safe, and really isn't to be encouraged...) Paul Moore
comment:4 by , 21 years ago
Logged In: NO > I expect it to require unusually complicated fractions > (ie, very large numerators and/or denominators). Indeed. Here's an example: 920182/8915 + 918356/4005 I'm sure it is possible to find fractions with smaller terms which also fail under the current algorithm but pass under the one I propose. > Your version does 1 extra GCD, 2 extra divisions, and 1 > extra multiplication. This could be a serious overhead > for the key case, rational<LongInt> where LongInt is a > user-defined unlimited-precision integer type. In this > case, overflow cannot happen. On the other hand, working > with bigger numbers *could* be more expensive (but I > doubt that it would be significantly so...) Yes. But, being so, why not drop the attempt to avoid overflow altogether? In my opinion this should be dealt with by adding a member to boost integer_traits which allows us to check at compile time whether we are dealing with limited- or unlimited-precision integers. Two specialized algorithms might then exist: one for unlimited- precision integers, which does not attempt at all to avoid overflow, and another for limited-precision integers, which tries as hard as possible to avoid such overflows. This should be a relatively simple thing to implement. Manuel Menezes de Sequeira Manuel.Sequeira@iscte.pt
comment:5 by , 17 years ago
| Status: | assigned → closed |
|---|
Logged In: YES user_id=91733 Regarding the discussion below, I think this issue can safely be declared as closed/rejected.
Note:
See TracTickets
for help on using tickets.
