3

I was writing a little function to calculate the binomial coefficiant using the tgamma function provided by c++. tgamma returns float values, but I wanted to return an integer. Please take a look at this example program comparing three ways of converting the float back to an int:

#include <iostream>
#include <cmath>

int BinCoeffnear(int n,int k){
    return std::nearbyint( std::tgamma(n+1) / (std::tgamma(k+1)*std::tgamma(n-k+1)) );
}

int BinCoeffcast(int n,int k){
    return static_cast<int>( std::tgamma(n+1) / (std::tgamma(k+1)*std::tgamma(n-k+1)) );
}

int BinCoeff(int n,int k){
    return (int) std::tgamma(n+1) / (std::tgamma(k+1)*std::tgamma(n-k+1));
}

int main()
{
    int n = 7;
    int k = 2;
    std::cout << "Correct: " << std::tgamma(7+1) / (std::tgamma(2+1)*std::tgamma(7-2+1)); //returns 21
    std::cout << " BinCoeff: " << BinCoeff(n,k); //returns 20 
    std::cout << " StaticCast: " << BinCoeffcast(n,k); //returns 20
    std::cout << " nearby int: " << BinCoeffnear(n,k); //returns 21

    return 0;   
}

why is it, that even though the calculation returns a float equal to 21, 'normal' conversion fails and only nearbyint returns the correct value. What is the nicest way to implement this?

EDIT: according to c++ documentation here tgamma(int) returns a double.

the.polo
  • 347
  • 3
  • 11
  • 2
    Maybe the result is something like `20.99999` or similar? Then casting will truncate the result. Remember that floating-point arithmetic on computers is prone to rounding errors. Related reading: [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – Some programmer dude Aug 23 '17 at 08:19
  • 1
    As some help to find out where the problem might be, split up your expression into smaller parts, and each smaller part into even smaller parts, and so on. Then assign each result to a temporary variable. That way you can easily step through the code line by line to see the result at each step. – Some programmer dude Aug 23 '17 at 08:22
  • 1
    Note that `BinCoeff` does the cast only on `std::tgamma(n+1)`. (and implicit conversion apply to result), no need to mix example with static_cast and C-Cast. – Jarod42 Aug 23 '17 at 08:23
  • 1
    Hi, @Someprogrammerdude in the first cout it prints exactly 21... – the.polo Aug 23 '17 at 08:24
  • 5
    @the.polo , "even though the calculation returns a float equal to 21" isn't quite right. If you remove your casts and int conversions and printed your float to the screen, you might be fooled by the default precision of `std::cout` ,and the act of printing a float of `20.999999999999975` will be rounded as it is formatted for display. Before you print out your floats, run `std::cout.precision(17);` (also see https://stackoverflow.com/questions/554063/how-do-i-print-a-double-value-with-full-precision-using-cout) – nos Aug 23 '17 at 08:25
  • Hi @nos, setting `std::cout << std::setprecision(std::numeric_limits::digits10 + 1) << std::tgamma(7+1) / (std::tgamma(2+1)*std::tgamma(7-2+1));` still returns 21 :/ – the.polo Aug 23 '17 at 08:36
  • 1
    I just want to point out that it depends on the type of the parameters and optimisation level: http://cpp.sh/9xfv7 – Jonas Aug 23 '17 at 08:42
  • 1
    I have [tested with 20 decimals fixed-precision output, and piece-wise calculations](http://coliru.stacked-crooked.com/a/1c5e4bcb985f6864). The result is exactly `21.00000000000000000000`. It might be a problem with your compiler? What compiler are you using? – Some programmer dude Aug 23 '17 at 08:49
  • 1
    @Someprogrammerdude Can you run that test with ` -O0`? – Jonas Aug 23 '17 at 08:51
  • 2
    @Jonas Ah yes, [that's very different then](http://coliru.stacked-crooked.com/a/d76816d30dbd4383). – Some programmer dude Aug 23 '17 at 08:56
  • @Someprogrammerdude I'm quite interested in knowing why compiler optimisation change the result, any thoughts? – Jonas Aug 23 '17 at 08:58
  • @Jonas The difference is the result of the ` std::tgamma(7+1)` call. One should start by checking what difference (if any) in the generated code between the two versions might be. – Some programmer dude Aug 23 '17 at 09:01
  • what a strange behaviour of the online compilers. I now tried to compile it on my machine where it showed me the float right away – the.polo Aug 23 '17 at 09:02
  • @Someprogrammerdude Well, looking at them assembly does not really help... With `-O3`: https://godbolt.org/g/bYHMgf with `-O0`: https://godbolt.org/g/aMrb3Z – Jonas Aug 23 '17 at 09:05

4 Answers4

3

Floating-point numbers have rounding errors associated with them. Here is a good article on the subject: What Every Computer Scientist Should Know About Floating-Point Arithmetic.

In your case the floating-point number holds a value very close but less than 21. Rules for implicit floating–integral conversions say:

The fractional part is truncated, that is, the fractional part is discarded.

Whereas std::nearbyint:

Rounds the floating-point argument arg to an integer value in floating-point format, using the current rounding mode.

In this case the floating-point number will be exactly 21 and the following implicit conversion would return 21.

The first cout outputs 21 because of rounding that happens in cout by default. See std::setprecition.

Here's a live example.


What is the nicest way to implement this?

Use the exact integer factorial function that takes and returns unsigned int instead of tgamma.

AMA
  • 4,114
  • 18
  • 32
3

The remark by @nos is on point. Note that the first line

std::cout << "Correct: " << 
 std::tgamma(7+1) / (std::tgamma(2+1)*std::tgamma(7-2+1));

Prints a double value and does not perform a floating point to integer conversion.

The result of your calculation in floating point is indeed less than 21, yet this double precision value is printed by cout as 21.

On my machine (x86_64, gnu libc, g++ 4.8, optimization level 0) setting cout.precision(18) makes the results explicit.

Correct: 20.9999999999999964  BinCoeff: 20 StaticCast: 20 nearby int: 21

In this case practical to replace integer operations with floating point operations, but one has to keep in mind that the result must be integer. The intention is to use std::round.

The problem with std::nearbyint is that depending on the rounding mode it may produce different results.

std::fesetround(FE_DOWNWARD);
std::cout << " nearby int: " << BinCoeffnear(n,k); 

would return 20.

So with std::round the BinCoeff function might look like

int BinCoeffRound(int n,int k){
            return static_cast<int>(
                            std::round(
                                    std::tgamma(n+1) /
                                       (std::tgamma(k+1)*std::tgamma(n-k+1)) 
                                  ));
}
Dima Chubarov
  • 16,199
  • 6
  • 40
  • 76
  • so what is the correct way to implement the binomial coefficiant? simply using `nearbyint`? – the.polo Aug 23 '17 at 09:00
  • 1
    @the.polo Updated the post. I believe you should use `std::round` since `std::nearbyint` would produce different results, depending on the current rounding mode. – Dima Chubarov Aug 23 '17 at 09:45
3

From this std::tgamma reference:

If arg is a natural number, std::tgamma(arg) is the factorial of arg-1. Many implementations calculate the exact integer-domain factorial if the argument is a sufficiently small integer.

It seems that the compiler you're using is doing that, calculating the factorial of 7 for the expression std::tgamma(7+1).

The result might differ between compilers, and also between optimization levels. As demonstrated by Jonas there is a big difference between optimized and unoptimized builds.

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • Perhaps add note on the dependency on optimisation level. Good catch though! – Jonas Aug 23 '17 at 09:14
  • Should not it be "not doing that" meaning that exact integer integral is **not** calculated? Clearly OP's `std::tgamma(7+1)` is not producing `21.0` – AMA Aug 23 '17 at 09:42
  • @AMA `std::tgamma(7+1)` is either producing `5040` (optimized build, equal to 7!), *or* something like `5039.99999999999909050530` (for an unoptimized build). It's this difference in calculation that leads to the problems for the OP. – Some programmer dude Aug 23 '17 at 09:45
  • @Someprogrammerdude oups, yes. I mean it's producing `5039.9999999999990905` and thus not doing the optimization. – AMA Aug 23 '17 at 09:47
0

the problem is on handling the floats. floats cant 2 as 2 but as 1.99999 something like that. So converting to int will drop out the decimal part.

So instead of converting to int immediately first round it to by calling the ceil function w/c declared in cmath or math.h.

this code will return all 21

#include <iostream>
#include <cmath>

int BinCoeffnear(int n,int k){
    return std::nearbyint( std::tgamma(n+1) / (std::tgamma(k+1)*std::tgamma(n-k+1)) );
}
int BinCoeffcast(int n,int k){
    return static_cast<int>( ceil(std::tgamma(n+1) / (std::tgamma(k+1)*std::tgamma(n-k+1))) );
}
int BinCoeff(int n,int k){
    return (int) ceil(std::tgamma(n+1) / (std::tgamma(k+1)*std::tgamma(n-k+1)));
}
int main()
{
    int n = 7;
    int k = 2;
    std::cout << "Correct: " << (std::tgamma(7+1) / (std::tgamma(2+1)*std::tgamma(7-2+1))); //returns 21
    std::cout << " BinCoeff: " << BinCoeff(n,k); //returns 20 
    std::cout << " StaticCast: " << BinCoeffcast(n,k); //returns 20
    std::cout << " nearby int: " << BinCoeffnear(n,k); //returns 21
    std::cout << "\n" << (int)(2.9995) << "\n";
}
a-tom
  • 1
  • Have a look at https://stackoverflow.com/a/1848762/2378300 regarding storing integers in doubles. – Jonas Aug 23 '17 at 09:09