2

I am working on a cryptocurrency and there is a calculation that nodes must make:

average /= total;
double ratio = average/DESIRED_BLOCK_TIME_SEC;
int delta = -round(log2(ratio));

It is required that every node has the exact same result no matter what architecture or stdlib being used by the system. My understanding is that log2 might have different implementations that yield very slightly different results or flags like --ffast-math could impact the outputted results.

Is there a simple way to convert the above calculation to something that is verifiably portable across different architectures (fixed point?) or am I overthinking the precision that is needed (given that I round the answer at the end).

EDIT: Average is a long and total is an int... so average ends up rounded to the closest second.

DESIRED_BLOCK_TIME_SEC = 30.0 (it's a float) that is #defined

user491880
  • 4,709
  • 4
  • 28
  • 49
  • maybe not `log2()`, but floating-point divisions are not portable. – Eugene Dec 01 '21 at 05:12
  • 2
    What types and ranges the variables and macros are? The exact result depends on what `e` is in the ratio `r = pow(2, R) * (1+e)`. Is it smaller or larger than `sqrt(2)-1`. – Aki Suihkonen Dec 01 '21 at 05:29
  • DESIRED_BLOCK_TIME is 30 (seconds), the value of average is anything from 1 second to 100's of seconds. – user491880 Dec 01 '21 at 05:41
  • 2
    In which units are these 'total' and 'average' calculated? Before the `average/=total` division? Fixed points, integer, floats? DESIRED_BLOCK_TIME is integer for the sake of analysis, but double for the sake if average is int, and ratio needs to be double. – Aki Suihkonen Dec 01 '21 at 05:54
  • Please [edit] your question instead of answering clarification questions in the comments. I'd say the only portable way is to do integer math (either use some form of fixed-point, or a library like MPFR). – chtz Dec 01 '21 at 08:23
  • @Eugene: How do you figure floating-point divisions are not portable? IEEE-754 specifies the numeric result of floating-point division completely. Portability issues arise from not conforming to IEEE-754 in one way or other (e.g., flushing subnormal results to zero, using extra precision, etc.). But if IEEE-754 arithmetic is used, the result should be reproducible. – Eric Postpischil Dec 01 '21 at 13:11
  • 1
    @EricPostpischil Yes, the problems arise from C++ compilers not following that IEEE standard with some flags. The OP specifically mentioned "--ffast-math". – Eugene Dec 01 '21 at 15:48

1 Answers1

2

For this kind of calculation to be exact, one must either calculate all the divisions and logarithms exactly -- or one can work backwards.

-round(log2(x)) == round(log2(1/x)), meaning that one of the divisions can be turned around to get (1/x) >= 1.

round(log2(x)) == floor(log2(x * sqrt(2))) == binary_log((int)(x*sqrt(2))).

One minor detail here is, if (double)sqrt(2) rounds down, or up. If it rounds up, then there might exist one or more value x * sqrt2 == 2^n + epsilon (after rounding), where as if it would round down, we would get 2^n - epsilon. One would give the integer value of n the other would give n-1. Which is correct?

Naturally that one is correct, whose ratio to the theoretical mid point x * sqrt(2) is smaller.

  • x * sqrt(2) / 2^(n-1) < 2^n / (x * sqrt(2)) -- multiply by x*sqrt(2)
  • x^2 * 2 / 2^(n-1) < 2^n -- multiply by 2^(n-1)
  • x^2 * 2 < 2^(2*n-1)

In order of this comparison to be exact, x^2 or pow(x,2) must be exact as well on the boundary - and it matters, what range the original values are. Similar analysis can and should be done while expanding x = a/b, so that the inexactness of the division can be mitigated at the cost of possible overflow in the multiplication...

Then again, I wonder how all the other similar applications handle the corner cases, which may not even exist -- and those could be brute force searched assuming that average and total are small enough integers.

EDIT Because average is an integer, it makes sense to tabulate those exact integer values, which are on the boundaries of -round(log2(average)).

From octave: d=-round(log2((1:1000000)/30.0)); find(d(2:end) ~= find(d(1:end-1))

1    2    3    6   11   22   43   85 170 340 679 1358 2716
5431 10862 21723 43445 86890 173779 347558 695115

All the averages between [1 2(  -> 5
All the averages between [2 3(  -> 4
All the averages between [3 6(  -> 3
..
All the averages between [43445 86890( -> -11

int a = find_lower_bound(average, table); // linear or binary search
return 5 - a;

No floating point arithmetic needed

Aki Suihkonen
  • 19,144
  • 1
  • 36
  • 57