Why BigFloat.to_s is not precise enough?
Asked Answered
L

1

9

I am not sure if this is a bug. But I've been playing with big and I cant understand why this code works this way:

https://carc.in/#/r/2w96

Code

require "big"

x = BigInt.new(1<<30) * (1<<30) * (1<<30)
puts "BigInt: #{x}"

x = BigFloat.new(1<<30) * (1<<30) * (1<<30) 
puts "BigFloat: #{x}"
puts "BigInt from BigFloat: #{x.to_big_i}"

Output

BigInt: 1237940039285380274899124224
BigFloat: 1237940039285380274900000000
BigInt from BigFloat: 1237940039285380274899124224

First I though that BigFloat requires to change BigFloat.default_precision to work with bigger number. But from this code it looks like it only matters when trying to output #to_s value.

Same with precision of BigFloat set to 1024 (https://carc.in/#/r/2w98):

Output

BigInt: 1237940039285380274899124224
BigFloat: 1237940039285380274899124224
BigInt from BigFloat: 1237940039285380274899124224

BigFloat.to_s uses LibGMP.mpf_get_str(nil, out expptr, 10, 0, self). Where GMP is saying:

mpf_get_str (char *str, mp_exp_t *expptr, int base, size_t n_digits, const mpf_t op)

Convert op to a string of digits in base base. The base argument may vary from 2 to 62 or from -2 to -36. Up to n_digits digits will be generated. Trailing zeros are not returned. No more digits than can be accurately represented by op are ever generated. If n_digits is 0 then that accurate maximum number of digits are generated.

Thanks.

Ludovick answered 12/10, 2017 at 13:40 Comment(3)
When you change the default precision it seems to work fine, right?Aubin
@Aubin yes, if I increase default precision it prints out just as many as meant by given precision, but if I turn the same float to int its like "max precision". I am just being confused about how and why.Ludovick
Because the precision only applies to BigFloat. It seems BigInt always prints all digits. In any case maybe we should increase the precision of BigFloat, you can open an issue for this.Aubin
W
1

In GMP (it applies to all languages not just Crystal), integers (C mpz_t, Crystal BigInt) and floats (C mpf_t, Crystal BigFloat) have separate default precision.

Also, note that using an explicit precision is better than setting a default one, because the default precision might not be reentrant (it depends on a configure-time switch). Also, if someone reads only a part of your code, they may skip the part with setting the default precision and assume a wrong one. Although I do not know the Crystal binding well, I assume that such functionality is exposed somewhere.

The zero parameter passed to mpf_get_str means to guess the value from the precision. I know the number of significant digits is proportional and close to precision / log2(10). Floating point numbers have finite precision. In that case, it was not the mpf_get_str call which made the last digits zero - it was the internal representation that did not keep such data. It looks like your (default) precision is too small to store all the necessary digits.

To summarize, there are two solutions:

  • Set a global default precision. Although this approach will work, it will require to either change the default precision frequently, or use one in the whole program. Both ways, the approach with the default precision is a form of procrastination which is going to have its vengeance later.
  • Set a precision on variable basis. This is a better solution than the former. Although it requires more code (1-2 more lines per variable initialization), it is going to pay back later. For example, in a space object tracking system, the physics calculations have to be super-precise, but other systems could use lower precision numbers for speed and memory saving.

I am still unsure what made the conversion BigFloat --> BigInt yield the missing digits.

Waikiki answered 26/9, 2018 at 18:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.