Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why BigFloat.to_s is not precise enough?

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.

like image 589
Ihor Tsykalo Avatar asked Oct 12 '17 13:10

Ihor Tsykalo


1 Answers

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.

like image 131
Top Sekret Avatar answered Nov 11 '22 14:11

Top Sekret