简体   繁体   中英

How can I multiply and divide integers without bigger intermediate types?

Currently, I'm developing some fuzzy logic stuff in C# and want to achieve this in a generic way. For simplicity, I can use float , double and decimal to process an interval [0, 1], but for performance, it would be better to use integers. Some thoughts about symmetry also led to the decision to omit the highest value in unsigned and the lowest value in signed integers. The lowest, non-omitted value maps to 0 and the highest, non-omitted value maps to 1. The omitted value is normalized to the next non-omitted value.

Now, I want to implement some compund calculations in the form of:

byte f(byte p1, byte p2, byte p3, byte p4)
{
    return (p1 * p2) / (p3 * p4);
}

where the byte values are interpreted as the [0, 1] interval mentioned above. This means p1 * p2 < p1 and p1 * p2 < p2 as opposed to numbers greater than 1, where this is not valid, eg 2 * 3 = 6 , but 0.1 * 0.2 = 0.02 .

Additionally, a problem is: p1 * p2 and p3 * p4 may exceed the range of the type byte . The result of the whole formula may not exceed this range, but the overflow would still occur in one or both parts. Of course, I can just cast to ushort and in the end back to byte , but for an ulong I wouldn't have this possibility without further effort and I don't want to stick to 32 bits. On the other hand, if I return (p1 / p3) * (p2 / p4) , I decrease the type escalation, but might run into a result of 0 , where the actual result is non-zero.

So I thought of somehow simultaneously "shrinking" both products step by step until I have the result in the [0, 1] interpretation. I don't need an exact value, a heuristic with an error less than 3 integer values off the correct value would be sufficient, and for an ulong an even higher error would certainly be OK.

So far, I have tried to convert the input to a decimal / float / double in the interval [0, 1] and calculated it. But this is completely counterproductive regarding performance. I read stuff about division algorithms , but I couldn't find the one I saw once in class. It was about calculating quotient and remainder simultaneously, with an accumulator. I tried to reconstruct and extend it for factorized parts of the division with corrections, but this breaks, where inidivisibility occurs and I get a too big error. I also made some notes and calculated some integer examples manually, trying to factor out, cancel out, split sums and such fancy derivation stuff, but nothing led to a satisfying result or steps for an algorithm.

Is there a

  • performant way
  • to multiply/divide signed (and unsigned) integers as above
  • interpreted as interval [0, 1]
  • without type promotion

?

If you are not careful, you will lost more time doing conversions that it would have take with regular operations.

That being said, an alternative that might make some sense would be to map value between 0 and 128 included (or 0 and 32768 if you want more precision) so that all value are essentially stored multiplied by 128 .

So if you have (0.5 * 0.75) / (0.125 * 0.25) the stored values for each of those numbers would be 64, 96, 16 and 32 respectively. If you do those computation using ushort you would have (64 * 96) / (16 * 32) = 6144 / 512 = 12 . This would give a result of 12 / 128 = 0.09375 .

By the way, you can ignore scaling for addition, substraction and division. For multiplication, you would do the multiplication as usual and then divide by 128. So for 0.5 * 0.75 you would have 64 * 96 / 128 = 48 which correspond to 48 / 128 = 0.375 as expected.

The code can be optimized for the platform particularly if the platform is more efficient with narrow numbers. And if necessary, rounding could be added to operation.

By the way since the scaling if a power of 2, you can use bit shifting for scaling. You might prefer to use 256 instead of 128 particularly if you don't have one cycle bit shifting but then you need larger width to handle some operations.

But you might be able to do some optimization if the most significant bit is not set for example so that you would only use larger width when necessary.

To answer your question as summarised: No.
You need to state (and rank) your overall goals explicitly (eg, is symmetry more or less important than performance ?). Your chances of getting a helpful answer improve with succinctly stating them in the question.
While I think Phil1970's you can ignore scaling for … division overly optimistic, multiplication is enough of a problem: If you don't generate partial results bigger (twice as big) as your "base type", you are stuck with multiplying parts of your operands and piecing the result together.
For ideas about piecing together "larger" results: AVR's Fractional Multiply .
Regarding …in signed integers. The lowest, non-omitted value maps to 0… …in signed integers. The lowest, non-omitted value maps to 0… , I expect that you will find, eg, excess -32767/32768-coded fractions even harder to handle than two's complement ones.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM