Want to scale int to int with integer math
Asked Answered
T

2

6

I am on SDCC 2.8.0, so very limited in memory and code size. Say I have an input value that ranges between 0 and 127, I want to scale it to 20 - 100. Normally I would do:

int scale(int input, int min, int max)
{
 // assuming max is always greater than min
 float range = (float)max - (float)min;
 int output = min + int((range / 127.f) * (float)input);
 return output;
}

By calling scale(64, 20, 100); I get 60, which is exactly half way between 20 and 100.

How can this be done without using the floating point numbers? Any bitshift magic?

Tire answered 28/1, 2014 at 17:37 Comment(2)
How large are your integers? What is the maximum value you expect for max - min?Gestation
Please, if possible, try to use a newer SDCC. Latest version is 3.3.0 and 3.4.0. is coming soon. SDCC has improved a lot since 2.8.0.Vachill
C
3

If (max-min)<(INT_MAX/127) then you can naivly multiply (max-min)*input before dividing /127
Else, you'll have to decompose operations in order to avoid overflow and undefined behavior...

In later case, a naive possibility would be to divide both multipliers by 127.

A=Q1*127+R1
B=Q2*127+R2
A*B = (Q1*Q2*127 + Q1*R2 + Q2*R1) * 127 + R1*R2
(A*B)/127 = Q1*Q2*127 + Q1*R2 + Q2*R1 + (R1*R2/127)

or in C:

unsigned int range=max-min;
unsigned int output = min
    + (range/127)*(input/127)*127
    + (range/127)*(input%127)
    + (range%127)*(input/127)
    + (range%127)*(input%127) / 127;

It's pretty sure that there are more efficient formulation with bit-shifting >>8, the compiler might already do it well, but maybe not so well and we might better help him:

A=Q1*128+R1
B= 0*128+R2 (because B<=127)
A*B = (Q1*R2) * (127+1) + R1*R2
(A*B)/127 = Q1*R2 + (Q1*R2 + R1*R2)/127

and in C:
EDIT
Ahem, my intention was to divide by 128, that is >>7, and I incorrectly wrote >>8 same for remainder which should be &0x7F not &0xFF
It's certainly better to be less obscure and just write /128 and %128 because we can trust the compiler to translate these ops into simple bit ops nowadays...

unsigned int range=max-min;
unsigned int high=(range / 128)*input;
unsigned int low =(range % 128)*input;
unsigned int output = min + high + (high+low)/127;

EDIT2
For balancing the distribution a little bit better, we might apply some sort of rounding rather than truncation like this:

unsigned int output = min + high + (high+low+63)/127;
Chloramphenicol answered 28/1, 2014 at 19:30 Comment(3)
Well, this works! I was taking a different route but this generates way less code than mine. Thank you!Tire
It is not always clear that directly mapping to the stated endpoints (as by (x-OldLow)*(NewHigh-NewLow)/(OldHigh-OldLow)+NewLow) is the right thing to do. Often, the values represent bins of some sort, in which case the actual span is not High-Low but is High-Low+BinWidth. Adjusting the map for this may give a better quality transformation.Gallo
@EricPostpischil Yes this is right, for example when range = 63, there will be a single input leading to max, and three leading to min. That fits original function, but we can do better... Oh, but you're talking of the separation between two bins when range > 127...Chloramphenicol
S
2

I understand this is an old thread, but I just wanted to share some tricks you can use to to scale with a float a bit more efficiently, if the scaling constants are fixed and known in advance. Compilers often use these tricks when there is a division with an integer literal, to avoid the usually expensive div instruction (which can take many dozens of cycles on many architectures).

Obviously, unless you really need to shave these several cycles off of each scaling operation, this is the definition of premature optimization.

Anyway, the idea is to change your floating point factor into an approximation which has a power of two in its denominator, so that you can replace the division with a multiplication (typically 1 cycle) and a right shift operation (again typically 1 cycle for operations on integers matching the architecture word size).

In your case, you would aim to replace the 1/127 part with a right shift, i.e. a division with a power of two. Since you need to scale with 80/127 (which is approx. 0.62992) and the input fits into 7 bits, you can choose something like 161/256 (I presumed you have a 16-bit controller, so I just multiplied 0.62992 with 256 since your input values all fit in the low byte of the word).

So the function then becomes:

// scale 0..127 into 20..100
uint8_t scale(uint8_t input)
{
    uint16_t low = input * 161;   // <- this will move the result into the high 8 bits
    low += 1 << 7;                // <- adding a "half bit" before shifting (like +0.5)
    low >>= 8;                    // <- cheap division by 256
    low += 20;                    // <- and finally, add offset

    return (uint8_t)(low);
}

On a 32-bit microcontroller, you would choose a larger factor to get a better approximation. It's often faster for the cpu/compiler to work with the native word size, because it doesn't need to truncate or extend register values to get to smaller integer sizes.

Since 127 needs 7 bits, you can choose a 24-bits denominator and still be sure the multiplied value will fit inside the 32-bit word, i.e.:

// 0.62992 == 10568325 / ‭16777216‬ == 10568325 / (1<<24)
uint8_t scale_32(uint8_t input)
{
    uint32_t low = input * 10568325;
    low += 1 << 23;
    low >>= 24;
    low += 20;
    return (uint8_t)(low);
}

You can use the godbolt online compiler to compare the assemblies of these functions in different compilers/architectures.

Spittle answered 29/1, 2019 at 0:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.