class 09 · hour nine of fifty
What if the point
cannot move?
There is another way to hold a fraction. It is older than floating point, and it is what every bank in this country runs on.
Nine programs. The hour ends by explaining, in one sentence, why floating point had to exist at all.
A shop sells a thousand items at Rs 49.99.
/* 01-till.c */
#include <stdio.h>
int main(void)
{
float till = 0.0f;
for (int i = 0; i < 1000; i++)
till += 49.99f;
printf("sales today 1000 items at Rs 49.99\n");
printf("should be Rs 49990.00\n");
printf("till says Rs %.4f\n", till);
printf("short by Rs %.4f\n", 49990.0f - till);
return 0;
}
sales today 1000 items at Rs 49.99
should be Rs 49990.00
till says Rs 49989.5430
short by Rs 0.4570
remember
- Ninety nine paise has no exact binary form, the same way one tenth does not.
- One day of sales and the till is short by forty six paise. Small, but it is real money and it will not reconcile.
- This is why no bank, no UPI app and no accounting system anywhere stores money in a float.
Ask what they would do about it before the next slide. Most of the room already knows the answer without knowing it has a name.
So do not store rupees. Store paise.
/* 02-paise.c */
#include <stdio.h>
int main(void)
{
long till = 0;
for (int i = 0; i < 1000; i++)
till += 4999;
printf("till says %ld paise\n", till);
printf("which is Rs %ld.%02ld\n",
till / 100, till % 100);
printf("exact? %s\n",
till == 4999000 ? "yes" : "no");
return 0;
}
till says 4999000 paise
which is Rs 49990.00
exact? yes
remember
- Nothing clever happened. We changed the unit and the fraction disappeared.
- The decimal point is not stored anywhere. It lives in the printf, and in your head.
- RBI systems, UPI messages and every ledger in the country carry amounts in paise as integers, for exactly this reason.
- You have just used fixed point arithmetic. It has a name and a general form.
The point being in the printf rather than the data is the whole idea of this hour. Say it plainly.
And it survives the arithmetic you actually do.
/* 03-gst.c */
#include <stdio.h>
int main(void)
{
long paise[] = { 89900, 24950, 149900, 99 };
printf("item float total exact, in paise\n");
for (int i = 0; i < 4; i++) {
long p = paise[i];
long gst = (p * 18 + 50) / 100;
float f = p / 100.0f;
printf("Rs %-7.2f Rs %-11.4f %ld + %ld = %ld\n",
f, f + f * 0.18f, p, gst, p + gst);
}
return 0;
}
item GST 18% total as paise, exactly
Rs 899.00 Rs 161.8200 Rs 1060.8201 89900 + 16182 = 106082 paise
Rs 249.50 Rs 44.9100 Rs 294.4100 24950 + 4491 = 29441 paise
Rs 1499.00 Rs 269.8200 Rs 1768.8201 149900 + 26982 = 176882 paise
Rs 0.99 Rs 0.1782 Rs 1.1682 99 + 18 = 117 paise
remember
- The float total is Rs 1060.8201. There is no such amount. Money has two decimal places and that is the end of it.
- In paise the whole calculation is integer arithmetic, and the rounding to the nearest paisa is a decision you make on purpose.
(p * 18 + 50) / 100 rounds to nearest. Drop the 50 and it truncates. GST law says which, and your type should not be guessing.
Rs 1060.8201 on an invoice is the point. Nobody can pay that, and nobody can file it.
Now the general form. Take an integer and imagine a point inside it.
IMAGINED HERE
BOTTOM 16 BITS
the fraction
One int32_t. The hardware sees a plain integer and always will.
We agree the bottom sixteen bits count fractions, and that agreement exists nowhere in the machine.
remember
- Written Q16.16: sixteen bits before the point, sixteen after.
- To store x, keep x times 65536. To read it back, divide by 65536.
- Paise were the same idea with a friendlier scale of 100 instead of 65536.
Same move as Class 4. The machine's representation and the human's need not match, so long as something translates at the edge.
What that looks like in a register.
/* 04-q16.c */
#include <stdio.h>
#include <stdint.h>
int32_t to_q(double x) { return (int32_t)(x * 65536.0 + (x < 0 ? -0.5 : 0.5)); }
double from_q(int32_t q) { return q / 65536.0; }
void show(char *label, int32_t q)
{
printf("%-12s raw %-12d = %d + %d/65536 = %.8f\n",
label, q, q >> 16, q & 0xFFFF, from_q(q));
}
int main(void)
{
show("Rs 1.00", to_q(1.0));
show("Rs 0.50", to_q(0.5));
show("Rs 49.99", to_q(49.99));
show("94.72 /L", to_q(94.72));
printf("\nthe gap is always %.10f, everywhere\n", 1.0 / 65536.0);
printf("largest value %.5f\n", from_q(2147483647));
return 0;
}
Rs 1.00 raw 65536 = 1 + 0/65536 = 1.00000000
Rs 0.50 raw 32768 = 0 + 32768/65536 = 0.50000000
Rs 49.99 raw 3276145 = 49 + 64881/65536 = 49.99000549
94.72 /L raw 6207570 = 94 + 47186/65536 = 94.72000122
the gap is always 0.0000152588, everywhere
largest value 32767.99998
remember
- Rs 49.99 is still not exact. Binary cannot spell ninety nine paise in any scheme, so pick a scale that suits the money.
- But the gap is the same size everywhere in the range. This is a ruler, which a float is not.
- Range is fixed and modest: about plus or minus thirty two thousand, in the same 32 bits a float uses to reach 1038.
The word ruler matters. Class 6 ended on a float not being one. This is what one looks like.
Arithmetic on it, at Rs 94.72 a litre.
/* 05-arith.c */
#include <stdio.h>
#include <stdint.h>
#define Q(x) ((int32_t)((x) * 65536.0 + 0.5))
#define R(q) ((q) / 65536.0)
int main(void)
{
int32_t price = Q(94.72); /* rupees per litre */
int32_t litres = Q(13.70);
printf("add %.4f\n", R(price + litres));
printf("sub %.4f\n", R(price - litres));
printf("mul %.4f\n", R((int32_t)(((int64_t)price * litres) >> 16)));
printf("div %.4f\n", R((int32_t)((((int64_t)price) << 16) / litres)));
printf("\nin double: %.4f %.4f %.4f %.4f\n",
94.72 + 13.70, 94.72 - 13.70,
94.72 * 13.70, 94.72 / 13.70);
return 0;
}
price Rs 94.7200 per litre
filled 13.7000 litres
price + price plain integer add 189.4400
price - litres plain integer subtract 81.0200
price * litres multiply, shift 16 1297.6637
price / litres shift 16, then divide 6.9139
the same four in double: 189.4400 81.0200 1297.6640 6.9139
remember
- Addition and subtraction are ordinary integer operations. No new hardware at all.
- Multiplication scales twice, so shift the result back by 16. Division needs the shift first.
- That is the appeal. A microcontroller with no floating point unit runs all of this, which is most embedded chips ever shipped.
- The pump display shows Rs 1297.66 either way. The difference is in the fourth decimal, and it compounds over a day.
A fuel dispenser is a legal metrology device. Its arithmetic is specified, audited, and almost always fixed point.
The point can sit anywhere. Watch what moving it does.
/* 06-trade.c */
#include <stdio.h>
#include <math.h>
int main(void)
{
char *words[] = { "13.4 crore", "83.9 lakh", "5.2 lakh",
"32 thousand", "2 thousand", "128", "8" };
printf("format largest value gap roughly\n");
for (int f = 4, w = 0; f <= 28; f += 4, w++)
printf("Q%-2d.%-2d %-15.0f %-15.10f %s\n",
31 - f, f, pow(2, 31 - f), pow(2, -f), words[w]);
printf("\nevery row: largest divided by gap = %.0f\n", pow(2, 31));
return 0;
}
remember
- Every step right doubles the precision and halves the range. Exactly.
- Largest value divided by gap is the same number in every row.
- You are not choosing how good it is. You are choosing where to spend a fixed budget.
format fraction largest value gap roughly
--------------------------------------------------------------------
Q27.4 4 13,42,17,728 0.0625000000 13.4 crore
Q23.8 8 83,88,608 0.0039062500 83.9 lakh
Q19.12 12 5,24,288 0.0002441406 5.2 lakh
Q15.16 16 32,768 0.0000152588 32 thousand
Q11.20 20 2,048 0.0000009537 2 thousand
Q7.24 24 128 0.0000000596 128
Q3.28 28 8 0.0000000037 8
--------------------------------------------------------------------
every row: largest value divided by gap = 2,14,74,83,648
an aside worth ten seconds
- 2,14,74,83,648 and 2,147,483,648 are the same quantity, grouped two different ways.
- Six marks, one number, from Class 1. Even the commas are a representation choice.
Read the last two columns down the page. One shrinks by sixteen as the other grows by sixteen, every time.
Why that trade is exact, and not merely usually true.
Thirty two bits give 232 patterns. That number cannot change.
Lay them out evenly and the span you cover is the count times the gap between them.
span = 232 × gap
remember
- The count is fixed, so halving the gap halves the span. No arrangement avoids this.
- Range and precision are not two dials. They are one dial with a label at each end.
- No configuration wins. Moving the point only decides which end of the problem you would rather be bad at.
Write span equals count times gap on the board. Three slides are consequences of it, and the last slide undoes it.
Two UPI transfers, and one type that cannot hold the total.
/* 07-overflow.c */
#include <stdio.h>
#include <stdint.h>
#define Q(x) ((int32_t)((x) * 65536.0 + 0.5))
#define R(q) ((q) / 65536.0)
int main(void)
{
int32_t a = Q(30000.0); /* two UPI transfers */
int32_t b = Q(20000.0);
printf("transfer one Rs %.2f\n", R(a));
printf("transfer two Rs %.2f\n", R(b));
printf("day total Rs %.2f\n", R(a + b));
printf("should be Rs 50000.00\n\n");
printf("the same sum in float = Rs %.2f\n", 30000.0f + 20000.0f);
printf("Q16.16 stops at Rs %.2f\n", R(2147483647));
return 0;
}
transfer one Rs 30000.00
transfer two Rs 20000.00
day total Rs -15536.00
should be Rs 50000.00
the same sum in float = Rs 50000.00
Q16.16 stops at Rs 32768.00
remember
- Fixed point overflow is integer overflow. It wraps, exactly as in Class 5, and says nothing.
- No infinity, no NaN, no flag anybody reads. Two positive transfers and a negative balance.
- Q16.16 tops out at Rs 32,768, which is below one day of ordinary UPI limits. Choosing the format is choosing the ceiling.
- The float handled it because fifty thousand is nowhere near its range. Range is what floats are for.
This is the wallpaper crash from Class 1, in a payments library rather than a colour value. Same wheel, same silence.
Inside its range, though, fixed point is often the more precise one.
/* 08-crossover.c */
#include <stdio.h>
#include <math.h>
int main(void)
{
double fixed_gap = 1.0 / 65536.0;
double v[] = { 0.01, 1.0, 100.0, 128.0,
256.0, 5000.0, 20000.0 };
printf("amount float gap Q16.16 gap finer\n");
for (int i = 0; i < 7; i++) {
float x = (float)v[i];
double fg = nextafterf(x, 2 * x) - x;
printf("Rs %-12g %-16.10f %-15.10f %s\n", v[i], fg, fixed_gap,
fg < fixed_gap ? "float" : "fixed point");
}
return 0;
}
remember
- Below Rs 128 the float is finer. Above it the fixed point is, all the way to its limit.
- The crossover is exactly 27, where the float's growing gap passes the fixed constant one.
- Neither is better. One is dense near zero, the other is even everywhere.
amount float gap Q16.16 gap finer
--------------------------------------------------------------
Rs 0.01 0.0000000009 0.0000152588 float
Rs 1 0.0000001192 0.0000152588 float
Rs 100 0.0000076294 0.0000152588 float
Rs 128 0.0000152588 0.0000152588 fixed point
Rs 256 0.0000305176 0.0000152588 fixed point
Rs 5000 0.0004882812 0.0000152588 fixed point
Rs 20000 0.0019531250 0.0000152588 fixed point
The highlighted row is the crossover, where the two gaps agree to the last digit.
And its error behaves completely differently from a float's.
/* 09-drift.c */
#include <stdio.h>
#include <stdint.h>
int main(void)
{
int N = 10000;
float f = 0.0f;
for (int i = 0; i < N; i++) f += 0.1f;
int64_t q = 0, tenth = 6554; /* Rs 0.10 in Q16.16 */
for (int i = 0; i < N; i++) q += tenth;
printf("float Rs %.6f off by %+.6f\n", f, f - 1000.0);
printf("Q16.16 Rs %.6f off by %+.6f\n",
q / 65536.0, q / 65536.0 - 1000.0);
return 0;
}
crediting Rs 0.10, ten thousand times
float Rs 999.902893 off by -0.097107
Q16.16 Rs 1000.061035 off by +0.061035
every fixed point addition was exact. the only error is
Rs 0.10 itself: 0.1000061035 instead of 0.10
that error, times 10000, is +0.061035
remember
- Every fixed point addition is an exact integer addition. No rounding happens at all.
- So the total error is the one representation error multiplied by the count. It matches to the last digit.
- The float's error came from the running total growing. The fixed point error does not care how large the total gets.
- In Class 8 the float error needed a seven band table to predict. Here it is one multiplication.
Predictable error is worth a great deal. It is why control systems, audio and metering equipment still use fixed point.
So neither one is the good one.
fixed point, when
- The range is known in advance. Money, sensors, audio samples, servo positions, energy meters.
- You need the same accuracy at every magnitude, not more of it near zero.
- The error must be predictable, or identical across machines.
- There is no floating point unit, or you cannot spare the power.
floating point, when
- You do not know the range in advance, or it spans many orders of magnitude.
- You want relative accuracy: three significant figures whether the number is tiny or enormous.
- You would rather lose precision than silently wrap around.
The Patriot clock in the last class was fixed point, counting tenths of a second. Now you know exactly what that sentence means.
Reopen the Dhahran program if you have it. 23 fractional bits, constant gap, error multiplied by tick count. All of it is on this slide.
Now look again at why the trade existed at all.
Span is the count times the gap. The count is fixed, so range and precision fight.
That argument only works because the gap is the same everywhere.
And nobody ever required that.
remember
- Uniform spacing is what forces range against precision. Give it up and the trade dissolves.
- What you usually want is a fixed number of significant figures, not a fixed gap.
- That is scientific notation, and putting it in 32 bits is a float.
- Floating point is not a better fixed point. It is the answer to a constraint that fixed point never questioned.
This is the sentence of the hour. We met floats three sessions ago and only now can they say why anyone bothered.
Every scheme in this arc has been
an answer to the same question.
Which four billion values should these patterns mean? Unsigned, two's complement, fixed point and IEEE 754 are four different answers, not four different topics.
Next session: what happens when a value has to move from one of them to another.
Send off: pick a Q format for an electricity meter reading up to 9999 units with two decimal places, and say what you gave up. Ariane closes next session.