Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use decnum for addition #1997

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

wtlangford
Copy link
Contributor

This is meant to be an example for how we might use decNumber to start implementing some arithmetic (and potentially some math functions, depending on what's available). It's not quite ready for merge- we should switch to using this in our constant folding code, and I still need to figure out what errors to look for on the context after the addition.

This adds a new jv_number_add function that adds two number jvs, consumes a reference for each of them, and returns a new number.

I changed a couple small things about allocation of literal numbers:

  • I switched to using the decContextDefault function to make sure we initialize the context properly (we had an error that used the wrong value for one of the fields- I'm not sure what the impact really was, though)
  • I reduced the number of units we allocate by 1, as there's always at least one in the decNumber itself.

@leonid-s-usov
Copy link
Contributor

This isn't enough. I've already implemented simple operations once, and what you are missing is changes to the parser.y where the constant folding is taking place.

@wtlangford
Copy link
Contributor Author

wtlangford commented Oct 22, 2019

what you are missing is changes to the parser.y

Yeah, I left a note for that in my PR comment- I haven't gotten to doing it yet, and wanted to vet out an implementation for actually doing the addition first.

@leonid-s-usov
Copy link
Contributor

Indeed, I've somehow glanced over that phrase. Cool

@leonid-s-usov
Copy link
Contributor

Do you have a clear view at which operations should be implemented lossless?

In the discussion before I've mentioned just the addition (including subtraction) and comparisons (already implemented)
Multiplication (and division) could be tricky given that the standard math library functions will always result in double precision results. How can we make it clear to the user which operations will be lossless and which not?

@wtlangford
Copy link
Contributor Author

My thoughts were to do just the basic arithmetic operations: +, -, *, /. I'm not sure why the behavior of the the standard math library would impact multiplication/division?

I've encountered a pretty rough issue with my digit calculation for addition, though- it's tricker than it appears. (How many digits is 9e2 + 9e-2?)

@leonid-s-usov
Copy link
Contributor

leonid-s-usov commented Oct 22, 2019

* may blow up the digits and as such might be unwanted, although I'd still go for lossless * and /

Re. digits calculation for the result, to my understanding for + and - the digits should be MAX(a.digits, b.digits) + 1, and for * and / it should be a.digits + b.digits

@leonid-s-usov
Copy link
Contributor

leonid-s-usov commented Oct 22, 2019

OK, I haven't analyzed the example correctly. You are right, we'll need to add some logic to calculate the upper digit bound.

I think we need to calculate the actual digit bounds for every number given its exponent and take the resulting range:

a_left = a.digits + a.exponent;
a_right = a.exponent;
b_left = b.digits + b.exponent;
b_right = b.exponent;

Then
for + and -: digits = MAX(a_left, b_left) - MIN(a_right, b_right) + 1
for your case it will be MAX(3, -1) - MIN(2, -2) + 1 = 6 (900.09 with one extra for the potential overflow)

for * and /: digits = a.digits + b.digits

@wtlangford
Copy link
Contributor Author

wtlangford commented Oct 22, 2019

* may blow up the digits and as such might be unwanted, although I'd still go for lossless * and /

Agreed about the mantissa, but I think that might be desirable.

Re. digits calculation for the result, to my understanding for + and - the digits should be MAX(a.digits, b.digits) + 1, and for * and / it should be a.digits + b.digits

That's what I remembered from school, too, but it turns out that only works if the two numbers have the same exponents. 😞

I've been doing some digit math, and I think I have the right approach now:

Let's assume that a is always the number with the most significant digit (and also that the numbers don't overlap each other- we'll relax this requirement later, but I want to showcase what I'm getting at):

We're interested in knowing how many digits are between the most significant digit of a and the least significant digit of b. Our number format always has a whole number for the mantissa (99.9e2 -> 999e1), so all of the digits are to the left of the exponent, which makes the math easier.

The position of the MSD of a is given by a.digits + a.exponent. The position of the LSD of b is given by b.exponent. Subtract those positions to get the number of digits in the result (and add 1 in case of a carry): a.digits + a.exponent - b.exponent + 1.

Let's fix that ordering requirement: Really, what we're interested in is the distance between the MSD of the two numbers and the LSD of the two numbers. So the math really becomes this: MAX(a.digits + a.exponent, b.digits + b.exponent) - MIN(a.exponent, b.exponent) + 1

It works with negative exponents, too: 9e-2 + 99e-3 = 189e-3. Using our digit math, we get:

MAX(1 + (-2), 2 + (-3)) - MIN(-2, -3) + 1
MAX(-1, -1) - MIN(-2, -3) + 1
-1 - (-3) + 1
3

Some other notable examples:

9e2 + 99e-3 = 900.099
MAX(1 + 2, 2 - 3) - MIN(2, -3) + 1
MAX(3, 1) - MIN(2, -3) + 1
3 - (-3) + 1
7 // This is one extra due to the carry assumption
9e1 + 9e1 = 18e1
MAX(1 + 1, 1 + 1) - MIN(1, 1) + 1
MAX(2, 2) - MIN(1, 1) + 1
2 - 1 + 1
2 // Perfect because of the carry

Edit: Ha, looks like we both got here at around the same time

@leonid-s-usov
Copy link
Contributor

yeah.. I've spent some time searching for appropriate library functions in the decNumber but apparenly they haven't gotten any. I had been thinking of using the decNumberReduce function to normalize the mantissa but as we have both eventually concluded it can be avoided by some simple math with digit and exponent. I believe this is good to go.

My only concern is whether the library will be smart enough to optimize the exponent as a result of the operation. The thing is, it might just use one of the operand's exponent and then end up with non-optimal digit count, throwing an exception when checking that the context doesn't provide enough storage.
This should be checked.

@wtlangford
Copy link
Contributor Author

My only concern is whether the library will be smart enough to optimize the exponent as a result of the operation. The thing is, it might just use one of the operand's exponent and then end up with non-optimal digit count, throwing an exception when checking that the context doesn't provide enough storage.
This should be checked.

Had this thought as well- I'll craft some test cases for it to see what happens.

@leonid-s-usov
Copy link
Contributor

If that's the case then we'll need to perform one additional operation - convert one of the numbers with decNumberQuantize method. It could be that this is the reason this method is there in the first place.

@wtlangford
Copy link
Contributor Author

Ah, wait, I make a new number for the result with the number of digits we calculate- I don't expect we'll have much in the way of issues. We may want to go ahead and normalize these numbers after the addition, though: 100e2 + 990e-3 -> 10000.990, which is a pretty odd behavior.

I'm not concerned about normalizing after addition, because we're already doing math at that point.

@wtlangford
Copy link
Contributor Author

Pushed up another commit that uses the new approach for calculating the new number of digits and also reduces the resultant number. It might be a bit expensive to perform for every bit of arithmetic, but this is the only time where we know it's safe to perform (if we did it at print-time, we wouldn't know if the trailing zeroes were from the input or not).

@@ -225,7 +225,7 @@ static block constant_fold(block a, block b, int op) {
int cmp = jv_cmp(jv_a, jv_b);

switch (op) {
case '+': res = jv_number(na + nb); break;
case '+': res = jv_number_add(jv_copy(jv_a), jv_copy(jv_b)); break;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it too late to copy the arguments here? jv_cmp above has already consumed those, hasn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it has. I'll fix that.

src/jv.c Outdated

decContext *ctx = DEC_CONTEXT();
decNumberAdd(&nlit->num_decimal, da, db, ctx);
decNumberReduce(&nlit->num_decimal, &nlit->num_decimal, ctx);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we believe that the operation above (the add) is able to fit the result into our calculated digits then the reduce should not be needed

Copy link
Contributor

@leonid-s-usov leonid-s-usov Oct 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After performing some tests I can tell that decNumberAdd will function correctly with our digits calculation and the only thing we should do after is decNumberTrim. We shouldn't call decNumberReduce as it will convert the number to its minimal scientific form which is not desirable IMO:

Using reduce:
61e2 + 00009876543210000000e-2 => 9.87654321061E+13

Using trim:
61e2 + 00009876543210000000e-2 => 98765432106100

Not using either:
61e2 + 00009876543210000000e-2 => 98765432106100.00

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the decNumberReduce was meant to address the trailing zero issue- I wasn't concerned about decNumberAdd functioning properly. decNumberTrim is the best option, I'll swap to that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants