Popup YouTube Video
Sheerpower Logo

REAL Numbers: Exact Decimal Arithmetic


REAL Numbers: Exact Decimal Arithmetic

Most programmers assume numbers behave exactly as written. In practice, this is not always true.

Most mainstream programming languages use IEEE binary floating-point for their standard floating-point numeric types.

For example:

print (0.1 + 0.2) - 0.3

In most mainstream languages, this expression does not produce exactly 0. Instead, it produces a very small leftover value because binary floating-point cannot represent many ordinary decimal fractions, such as 0.1, exactly.

Sheerpower's REAL data type is designed around decimal arithmetic rather than binary floating-point. Ordinary decimal values are represented exactly, and within REAL's normal fixed-decimal range of up to 54 digits before and 16 digits after the decimal point, addition and subtraction are exact. This prevents the small accumulated errors commonly associated with binary floating-point and allows results to be compared directly using =, without epsilon tolerances.

Designed for business applications. Sheerpower's REAL is a decimal type designed for money, measurements, quantities, and other calculations where predictable decimal behavior matters. It does not use binary floating-point: REAL represents decimal values exactly, so 0.1 is exactly one tenth.

In its normal fixed-decimal representation, REAL supports up to 54 digits before and 16 digits after the decimal point simultaneously. Addition and subtraction within this range are exact. Multiplication and division are rounded once, half-to-even, at the sixteenth place when the result needs more places than that, and the result is marked *Rounded* (a result smaller than 0.00000001 keeps 64 significant digits instead, below). divr() keeps a quotient exact when you ask for it.

When a calculation requires greater dynamic range or additional decimal precision, REAL automatically switches to scientific notation while retaining up to 64 significant decimal digits. The exponent can have a magnitude of up to 1,000,000,000 (a one followed by a billion zeros), allowing REAL to represent extremely large and extremely small values.

For example:

print pi + 1e53

Output:

100000000000000000000000000000000000000000000000000003.1415926535897932

Rounding: Half-to-Even, and round()

Some operations, particularly multiplication and division, can produce more digits than REAL can retain. When rounding is necessary, Sheerpower uses half-to-even rounding, producing predictable results while preserving up to 64 significant decimal digits.

For example:

print sqrt(2)

Output:

1.414213562373095

sqrt(2) has an infinite decimal expansion, so REAL keeps it correctly rounded to sixteen places: 1.4142135623730950. The printed 1.414213562373095 shows fifteen of them because the sixteenth is a zero, and print leaves off trailing zeros. If that value is later used in another calculation, any additional rounding that becomes necessary is performed consistently according to REAL's decimal arithmetic rules.

Half-to-even sends an exact tie to the even digit, so ties go up as often as down and rounding adds no bias to a long column of results. The round() function is different on purpose: it rounds a tie away from zero (half-up), the rule business code expects, so round(2.5, 0) is 3 where the arithmetic rule would give 2:

print 1.0000000000000005 / 2 print 1.0000000000000015 / 2 print round(2.5, 0), round(3.5, 0), round(-2.5, 0)

Output:

0.5000000000000002 0.5000000000000008 3 4 -3

For business calculations such as currency, results can still be rounded explicitly to the required working scale — for example, two decimal places — when that scale is part of the application's requirements.

A whole-number power is computed on all of its digits and rounded only once, at the end, so even a small base raised to a large power keeps every significant digit it can, and a negative power is a single divide. (x ^ 2 is the same one multiply as x * x.)

print 0.5 ^ 100 print 0.5 ^ -100 print 0.5 ^ -100 = 2 ^ 100

Output:

7.888609052210118054117285652827862296732064351090230047702789307e-31 1267650600228229401496703205376 1

How Many Digits a Value Keeps

Very small results keep 64 digits. The sixteen places are an absolute grid: near 1 they hold sixteen significant digits, but a value like 0.0000000000000012 would keep only two of them. So a result smaller than 0.00000001 (10-8) that the sixteen places cannot hold exactly is kept in scientific form instead, with 64 significant digits, rounded once. A small value the places do hold exactly stays on them, and from 10-8 up a result uses the sixteen places as always:

x = pi ^ 30 print 1 / x print 1 / 30 print 1 / 300000000 print 0.00000001 / 4

Output:

1.217597700325186743805050027998895815974103291905792193890430296e-15 0.0333333333333333 3.333333333333333333333333333333333333333333333333333333333333333e-9 0.0000000025

How many digits a rounded result keeps. Above 0.00000001 the sixteen places count from the decimal point, so the size of a result decides how many significant digits it keeps. A third, at several sizes:

print 1000 / 3 print 1 / 3 print 1 / 30 print 1 / 3000 print 1 / 30000000 print 1 / 300000000

Output:

333.3333333333333333 0.3333333333333333 0.0333333333333333 0.0003333333333333 0.0000000333333333 3.333333333333333333333333333333333333333333333333333333333333333e-9
Size of the resultSignificant digits kept
just under 105470 (54 + 16)
100 to 1,00019 (3 + 16)
1 to 1017
0.1 to 116
0.01 to 0.115
0.0001 to 0.00113
0.00000001 to 0.00000019
below 0.0000000164, in scientific form

That matters when a small ratio will be multiplied back up. A 5% annual rate per minute lies between 0.00000001 and 0.0000001, so it keeps nine digits (the ninth is a zero), and multiplied back by the 525,600 minutes in a year it misses 0.05 in the eleventh place. The same rate per second is below 0.00000001, keeps 64 digits, and comes back as 0.05. divr() (Exact Fractions, below) keeps the exact fraction at any size:

annual_rate = 0.05 per_minute = annual_rate / 525_600 per_second = annual_rate / 31_536_000 print per_minute print per_second print per_minute * 525_600 print per_second * 31_536_000 print divr(annual_rate, 525_600) * 525_600

Output:

0.000000095129376 1.585489599188229325215626585489599188229325215626585489599188229e-9 0.0500000000256 0.05 0.05

The rule: when a small ratio will be multiplied back up, make it with divr().

How many of those digits are right. Every result is the correctly rounded answer for the values it was given. When one of those values was itself rounded earlier, the extra digits carry that rounding forward: they are exact for the rounded value, not for the number you had in mind. Here x holds 1/3 to sixteen places, so x / 700000000 agrees with 1/2100000000 for sixteen digits and then follows the rounded x. isrounded() says a value was rounded somewhere along the way; when every digit matters, keep the exact fraction with divr() (below) or compute from the original numbers:

x = 1 / 3 print x / 700000000 print 1 / 2100000000 print divr(1, 3) / 700000000 print isrounded(x / 700000000)

Output:

4.761904761904761428571428571428571428571428571428571428571428571e-10 4.761904761904761904761904761904761904761904761904761904761904762e-10 4.761904761904761904761904761904761904761904761904761904761904762e-10 1

Overflow and Underflow

Overflow does not silently wrap. If a result is larger than REAL's supported numeric range, Sheerpower detects it and raises a runtime exception. A result too small for the range — below 1e-1000000000 — becomes 0, the way IEEE floating point and Python's decimal type handle underflow: it is closer to 0 than to any value a REAL can hold.

smallest = val('1e-1000000000') print smallest print smallest / 10

Output:

1e-1000000000 0

Functions Computed Through a Double

The statistics built on a square root — stats$pcorr(), stats$spearman() and stats$linreg_r2() — work from exact sums and round only once, so every digit they print is right. A very small result that comes from a C double (exp(), log(), the trigonometric functions) keeps the double's fifteen digits, not 64. Near 1 the logarithms do better than the double alone could: 1.000000001 is not exact in binary, and a language that hands its double to the logarithm — NumPy, for one — prints 1.000000082240371e-9, right to seven digits. Sheerpower still holds the exact digits of x, so log(), log2() and log10() add back the error of the conversion: log(1.000000001) is 9.999999995e-10, every digit right.

Functions computed through a double. exp(), log(), log2(), log10(), sin(), cos(), tan(), asin(), acos(), atn(), sinh(), cosh() and tanh() work through a C double, so their results carry the double's fifteen significant digits and are marked *Rounded* unless the double is exact (cos(0) is a plain 1). sec(), csc() and cot() divide one of those results, so they print sixteen places but are no more accurate than the double. A double's range is not theirs, though: past it exp(), a non-whole power and the logarithms split the value into its power of ten and the rest, and only the rest goes through the double, so exp(1000) is 1.97007111401705e+434 and 10 ^ 400.5 is 3.16227766016838e+400, still to fifteen digits. sinh() and cosh() do stop there: sinh(1000) raises "Numeric value out of range for this function". sqrt() is not on the list: it works in whole-number arithmetic and is correctly rounded.

print exp(1), sin(1), sec(1) print isrounded(exp(1)), isrounded(cos(0))

Output:

2.71828182845905 0.841470984807897 1.8508157176809246 1 0

Exact Fractions: division that stays exact

Divide two whole numbers and the answer often will not fit in the sixteen digits a decimal real provides: 1/3 becomes 0.3333333333333333, and three of those add up to 0.9999999999999999 -- not 1. Many languages hand you the rounded number and leave you to notice. Sheerpower offers the exact fraction when you ask for it by name.

divr(a, b) divides and keeps the EXACT FRACTION when the quotient does not terminate, and the fraction stays exact through the add, subtract, multiply, divide and whole-number powers that follow, while its denominator is at most 4,294,967,295 (232 - 1). The / operator is always the sixteen-place decimal divide, so a program says exactly where it wants exactness:

x = divr(1, 3) y = x + x + x print y print 1 / 3 * 3

Output:

1 0.9999999999999999

What joins a fraction exactly. Whole numbers of any size (past 1054, in scientific form, too), other fractions, and short decimals: a decimal counts as the exact fraction it is written as (0.25 is 1/4, 0.05 is 1/20) whenever that fraction's denominator is at most 4,294,967,295 — every decimal of nine places or fewer. That holds for the arguments of divr() and for anything a fraction meets:

x = divr(1, 3) print fraction$(x + divr(1, 2)) print fraction$(x + 0.25) print fraction$(divr(0.05, 12)) print divr(0.05, 12) * 12 print 0.05 / 12 * 12 print divr(1, 7) * 10 ^ 54 * 7

Output:

5/6 7/12 1/240 0.05 0.0500000000000004 1000000000000000000000000000000000000000000000000000000

A quotient that terminates is simply a decimal: divr(1, 2) is 0.5 and divr(6, 3) is 2 (typeof$() says *Narrow*). Because 0.5 is a short decimal, x + divr(1, 2) above is still exactly 5/6.

The same holds for every result, not only for divr(): when arithmetic on a fraction lands on a value that terminates, the value is stored as that decimal — still exact, and fraction$() still shows it as a fraction. A monthly rate of 1/240 times 7 is still a fraction; times 12 it is 12/240 = 1/20, the decimal 0.05; and divr(1, 3) + divr(1, 6) is the decimal 0.5:

rate = divr(0.05, 12) print typeof$(rate * 7) print typeof$(rate * 12) print rate * 12 print fraction$(rate * 12) print typeof$(divr(1, 3) + divr(1, 6))

Output:

Name:*Literal*, Dtype:Real, *Rational* 7/240 Name:*Literal*, Dtype:Real, *Narrow* 0.05 1/20 Name:*Literal*, Dtype:Real, *Narrow*

Where a fraction becomes a decimal. A LONG decimal — one whose fraction would need a bigger denominator, such as the sixteen places of a rounded result like 0.3333333333333333 — is taken as the decimal it is: beside it, in arithmetic and in comparison, a fraction is the sixteen-place decimal it rounds to. So a value printed and read back compares equal to the fraction that printed it. A result whose denominator would pass 4,294,967,295 is rounded the same way — once, from its exact value, however large the power that made it — and every print, str$() and f$() shows the decimal:

x = divr(1, 3) print x + 0.3333333333333333 print x = 0.3333333333333333 print fraction$(divr(1, 3) ^ 20) print divr(1, 3) ^ 21 print divr(4, 3) ^ 166 print divr(4, 3) ^ 166 = 4 ^ 166 / 3 ^ 166

Output:

0.6666666666666666 1 1/3486784401 9.559906635974804377740857437469456355220551341835985612272828719e-11 549326154308536733621.4898728569424319 1

How long a fraction lasts. A sum of fractions needs a denominator every term divides, so with prime denominators they multiply and the sum reaches the limit quickly. The sum of 1/p over the primes from 3 to 29 is still exact; adding 1/31 needs a denominator of 100,280,245,065, and the sum becomes its sixteen-place decimal:

dim primes(*) fill primes with 3, 5, 7, 11, 13, 17, 19, 23, 29 prime_sum = 0 for prime_no = 1 to size(primes) prime_sum += divr(1, primes(prime_no)) next prime_no print fraction$(prime_sum) prime_sum += divr(1, 31) print prime_sum print isrounded(prime_sum)

Output:

3343015913/3234846615 1.0656968363881611 1

A fraction lives only in memory. Written to a file, or anywhere as text, it is its sixteen-place decimal, and that is what reads back: equal to the fraction, but times 3 it no longer gives 1:

third = divr(1, 3) open file out_ch: name '@third.txt', access output print #out_ch: str$(third) close #out_ch open file in_ch: name '@third.txt' line input #in_ch: third_text$ close #in_ch third_read = val(third_text$) print third_text$ print third_read = third print third * 3, third_read * 3

Output:

0.3333333333333333 1 1 0.9999999999999999

Very small results. A fraction smaller than 0.00000001 follows the rule for every very small result: its decimal is what the / divide of the same two numbers gives, 64 significant digits in scientific form (*Sci*), marked *Rounded* unless those digits are the whole value. A tiny fraction does not turn into 0 (only a result below 1e-1000000000, the end of REAL's range, does):

small = divr(1, 3) ^ 40 print small print small = 1 / 3 ^ 40 print typeof$(small) print divr(1, 4 * 10 ^ 16) print isrounded(divr(1, 4 * 10 ^ 16))

Output:

8.225263339969959081282058400607250240380335470448824242293174741e-20 1 Name:SMALL, Dtype:Real, *Sci*, *Rounded* 2.5e-17 0

typeof$() says *Rational*, fraction$() shows the fraction, numerator() / denominator() take it apart, decimal() rounds it, and divr() maps over an array (divr(v, 3)).

Those functions take a value in scientific form too. A whole one is its own fraction, written as str$() writes it. A rounded value smaller than 0.00000001 has 64 significant digits, so its denominator has 72 digits or more: fraction$() writes it in e-notation, the way str$() writes so large a number, and fraction() of a value below about 10-10 is 0, the closest fraction any allowed denominator can make:

print fraction$(10 ^ 60) print fraction$(divr(1, 3) ^ 40) print denominator(divr(1, 3) ^ 40) print fraction(divr(1, 3) ^ 40, 1000)

Output:

1000000000000000000000000000000000000000000000000000000000000 8225263339969959081282058400607250240380335470448824242293174741/1e+83 1e+83 0

The Modes and the *Rounded* Mark

typeof$() names the mode a value is in: *Narrow* (18 integer digits and 16 places), *Wide* (54 integer digits), *Sci* (64 significant digits with an exponent), *Rational* (an exact fraction), *Complex*, *NaN* and *Inf*. The modes are automatic: a value moves between them by its size and its origin, never by a declaration.

One more marker can follow the mode: *Rounded*, when Sheerpower had to round in making the value — a multiply or divide that needed more than sixteen places, a result past 64 significant digits, a square root that is not exact, a function computed through a double whose answer is inexact (sin(1), but not cos(0)), a fraction turned into its decimal, or text with more digits than fit. Arithmetic on a rounded value carries the mark on. The functions that round because you asked — round(), truncate(), int(), floor(), ceil() — never add it; they keep their input's, because a value rounded earlier can sit on the wrong side of the boundary: (1 / 3) * 7.5 is 2.4999999999999998, not 2.5, so round() gives 2 where 7.5 / 3 rounds to 3. Zero never carries the mark: (1 / 3) * 0 is a plain 0. The mark says how a value was made, not what it is: a rounded 0.3333333333333333 equals one typed in, and lookups and unique() treat the two as one value. It lives only in memory — text, files and table fields do not carry it.

Arrays, statistics and complex values follow the same rule. sort() keeps each element's mark; a statistic — stats$max(), stats$median(), stats$percentile() and the rest — is marked when its data holds a rounded value; matmul() marks a product that needed more than sixteen places. Complex arithmetic keeps thirty digits in each part and marks a result it had to round to fit them, and a complex function computed through doubles is marked when the double is inexact.

third = 1 / 3 print typeof$(third) print typeof$(sqrt(4)) print typeof$(sqrt(2)) y = round(1.2345, 3) print typeof$(y) print third = 0.3333333333333333 print third * 7.5 print round(third * 7.5, 0), round(7.5 / 3, 0) print isrounded(third * 0)

Output:

Name:THIRD, Dtype:Real, *Narrow*, *Rounded* Name:*Literal*, Dtype:Real, *Narrow* Name:*Literal*, Dtype:Real, *Narrow*, *Rounded* Name:Y, Dtype:Real, *Narrow* 1 2.4999999999999998 2 3 0
third = 1 / 3 dim v(*) fill v with 0.5, third, 0.25 dim s(*) s = sort(v) print typeof$(s(1)) print typeof$(s(2)) print typeof$(stats$max(v)) fill v with 0.5, 0.75, 0.25 print typeof$(stats$max(v)) print typeof$((1 + 2i) / (3 + 4i)) print typeof$((1 + 2i) / (3 + 7i)) print typeof$(sqrt(-4 + 0i)) print typeof$(sqrt(-2 + 0i))

Output:

Name:*Literal*, Dtype:Real, *Narrow* Name:*Literal*, Dtype:Real, *Narrow*, *Rounded* Name:*Literal*, Dtype:Real, *Narrow*, *Rounded* Name:*Literal*, Dtype:Real, *Narrow* Name:*Literal*, Dtype:Real, *Complex* Name:*Literal*, Dtype:Real, *Complex*, *Rounded* Name:*Literal*, Dtype:Real, *Complex* Name:*Literal*, Dtype:Real, *Complex*, *Rounded*

isrounded(x) asks the question directly: it returns 1 when x carries the mark and 0 otherwise, and like isnan() it works on a whole array — stats$sum(isrounded(v)) counts the rounded values and filter(v, not isrounded(v)) keeps the exact ones. The mark also decides how a very large value prints: an exact 64-digit integer, such as 1e63 or the 304th Fibonacci number, prints every digit, while one that had to be rounded to fit prints in scientific notation.

third = 1 / 3 print isrounded(third), isrounded(0.05), isrounded(divr(1, 3)) dim v(*) fill v with third, 0.5, 1 / 7, 0.25 print isrounded(v) print stats$sum(isrounded(v)) print filter(v, not isrounded(v)) big = 1e63 print big print big + 0.123 print isrounded(big), isrounded(big + 0.123)

Output:

1 0 0 1 0 1 0 2 0.5 0.25 1000000000000000000000000000000000000000000000000000000000000000 1e+63 0 1

Not-a-Number and Infinity

A REAL can hold two special values besides ordinary numbers: NaN ("not-a-number") and infinity. They are written _nan, _inf, and -_inf.

They are values you put in. A constant, val("NaN"), a CSV cell that reads Inf, a float field read from a table — each of those can bring one into a program. What Sheerpower does not do is manufacture them from ordinary numbers: 1 / 0 still raises DIVBY0, sqrt(-1) still raises a range error, and assigning either value to an INTEGER raises NUMOVER. So a NaN or an infinity in your data is always something that was put there, never a calculation that went wrong behind your back.

Once one is present, it propagates through arithmetic the way IEEE 754 specifies:

  • any expression containing a NaN is a NaN
  • _inf + 1 is _inf
  • _inf - _inf is _nan
  • 1 / _inf is 0

div0inf(a, b) is the division that produces these values on purpose: _inf or -_inf for a nonzero number over zero, _nan for 0 / 0, and the ordinary exact quotient otherwise. It is the companion of div0(a, b), which returns 0 for division by zero instead. Because you have to call it by name, every place an infinity can enter a calculation is visible in the source.

A NaN is never equal to anything, not even to another NaN. Two variables that both hold _nan compare unequal, so an if x = y test quietly passes over exactly the values you were looking for. Sheerpower will not let you write the literal form at all: if x = _nan is a compile-time error, because the answer is always "no" and the comparison is therefore always a mistake. Use isnan(var), which returns 1 when the value is a NaN and 0 otherwise.

Infinity is different. It compares normally, so if x = _inf is legal and does what it says. But it tests for one sign only — isinf(var) returns 1 for both _inf and -_inf, which is usually the question you meant to ask.


Complex Numbers

Sheerpower REALs can also represent complex numbers. Complex-number syntax, arithmetic, functions, precision, and FFT support are covered in the separate Processing Complex Numbers tutorial Processing Complex Numbers.


Complex numbers, and the two specials. The same REAL holds a complex value: write the imaginary unit as a suffix (3 + 4i), or build one with complex(re, im), and the four operators, abs(), conj(), the transcendental functions and the fft() transforms follow, with thirty-digit exact-then-round arithmetic and no type to declare. And a REAL can hold NaN and Inf: they never arise from finite arithmetic (1 / 0 still raises), only from the specials _nan / _inf, from data that carried them, or from div0inf(), and then they propagate by the IEEE rules. The complex page (Processing Complex Numbers) tells the whole story, with the precision, the ranges and the transforms.
z = 3 + 4i print z * (3 - 4i), abs(z), typeof$(z) ! 25+0i 5 Name:Z, Dtype:Real, *Complex* print sqrt(-4 + 0i), conj(z) ! 0+2i 3-4i print _inf - _inf, 1 / _inf, typeof$(_nan) ! NaN 0 Name:*Literal*, Dtype:Real, *NaN*

Inside a REAL: the Internal Representation

Sheerpower stores REAL numbers using multiple 64-bit integers. In its normal fixed-point representation, a REAL consists of:

  • IP — The integer portion of the number (digits to the left of the decimal point), supporting approximately 54 decimal digits.
  • FP — The fractional portion of the number (digits to the right of the decimal point), stored as an integer scaled by 1016. This preserves up to 16 decimal digits exactly.

Together, these provide up to 70 significant decimal digits.

When still larger values are required, REAL automatically uses scientific notation while retaining up to 64 significant digits. The exponent can have a magnitude of up to 1,000,000,000, allowing values on the order of 10^1000000000 as well as correspondingly tiny values.

That is why the two limits differ: just under 1054 a fixed-decimal value holds 70 digits (54 + 16), while past it the scientific form holds 64, so at 1054 only nine decimal places are left. 10 ^ 54 + 0.123456789123 prints as 1000000000000000000000000000000000000000000000000000000.123456789 and is marked *Rounded*.

value = 13.599 IP = 13 FP = 5990000000000000 // 0.599 × 10^16 Example with more decimals: value = 3.1415926535897932 IP = 3 FP = 1415926535897932 (exactly 16 digits after decimal) Negative values keep the same IP and FP; one sign bit says negative: value = -13.599 IP = 13 FP = 5990000000000000 sign = negative value = -0.5 IP = 0 FP = 5000000000000000 sign = negative

The sign is kept once, as the top bit of the highest word; IP and FP always hold the magnitude, so -13.599 and 13.599 differ in that one bit. The ip() and fp() functions give the parts with the sign: ip(-13.599) is -13 and fp(-13.599) is -0.599.

This separation allows Sheerpower to maintain complete decimal accuracy capped at 54 digits before and 16 digits after the decimal point, predictably.

In standard floating-point math, "magnitude mismatch" errors occur when numbers of vastly different scales are mixed. For example, adding a micro-transaction to a national balance sheet might result in the smaller value simply disappearing due to the limitations of binary representation—technically known as loss of significance (absorption). By storing the integer and fractional portions separately, Sheerpower REALs prevent magnitude mismatch. You can safely add one quintillion and 0.000000000000123 without losing a single digit of precision.

Visualizing the Structure

The illustration below shows how a REAL value is stored:

REAL = 13.599

A REAL uses multiple 64-bit integers.
IP (Integer Parts)
13
FP (Fractional Part)
5990000000000000

Why This Is Important

  1. Reliable financial systems — every penny is preserved
  2. Accounting that stays accurate—$0.01 stays $0.01 forever, no drift over millions of transactions
  3. Clean and predictable results for statistical and data-processing workloads
  4. Comparisons you can rely on — sums and differences compare exactly with =; a rounded product or quotient compares as the sixteen-place decimal it is, so 1 / 3 * 3 = 1 is false and divr(1, 3) * 3 = 1 is true
  5. Totals that do not depend on order — exact addition gives the same sum however the values are grouped, so a threaded stats$sum() or stats$mean() returns the same digits however the array is split
  6. Data interchange that keeps every digit — str$() writes every digit a value holds (a fraction, its sixteen-place decimal) and val() reads it back equal, across JSON, CSV, databases and APIs. The other side must read decimal text exactly too: many JSON parsers read numbers into doubles

(Show/Hide Performance Comparisons)

Why Sheerpower REALs Became Fast Enough to be Practical

Exact decimal arithmetic is not new. Decimal money types have existed for decades — COBOL's packed decimal in the 1960s, hardware decimal instructions on mainframes, and 128-bit decimal types like C#'s decimal since the early 2000s. The challenge was never correctness. It was speed.

Sheerpower REALs store values using multiple 64-bit integers. On a 32-bit machine this works correctly, but each 64-bit value must be assembled from two 32-bit registers, so every operation expands into multi-word arithmetic — carry propagation, register spills, and helper routines. The math is exact, but it runs well below the speed of native floating-point hardware. That performance gap, not any limit on accuracy, is one reason mainstream languages committed early to IEEE 754 binary floating-point (standardized in 1985) and to heap-allocated arbitrary-precision libraries like BigDecimal. The others were the floating-point hardware already built into CPUs and scientific computing, which needs range more than decimal digits. Fast decimal was the expensive option.

What changed: Modern x86-64 CPUs perform 64-bit integer operations at full register speed. The common Narrow representation (18.16) is particularly fast on modern 64-bit processors. Its primary numeric components fit naturally into 64-bit registers, allowing many ordinary REAL operations to use native integer instructions directly. Wider REAL values use additional internal words as required.

Addition and subtraction collapse to a few integer instructions. Multiplication uses a native 64-bit multiply whose 128-bit result is scaled down and half-to-even rounded back into REAL arithmetic. In both cases, the multi-word overhead that made decimal slow on 32-bit hardware largely disappears.

Why other languages did not change course: Billions of lines of existing code depend on floating-point defaults, numeric promotion rules, and decades of performance assumptions. Backward compatibility locked those languages into their original numeric models, even as the hardware economics shifted underneath them.

Sheerpower REALs were designed for the 64-bit era. Exact decimal money math was always achievable — Sheerpower's contribution is making it run at native-integer speed for the high-volume transactional workloads where that speed matters.


Exact REAL Math Across Extreme Magnitudes

// Sheerpower Exact REAL vs. Standard Double Simulation // Purpose: Process 10 million Micro-Fees against a Global Balance // Standard double precision would lose precision here: // 1. $105 Trillion requires 15 digits of precision. // 2. The micro_fee is at the 5th decimal place. // 3. Total precision needed: 20 digits (exceeds Double's ~16 digit limit). balance = 105_000_000_000_000 // $105 Trillion (IP=105T, FP=0) micro_fee = 0.00001 // For doubles, an "Impossible" binary fraction iterations = 10_000_000 print "Processing 10 million Transactions..." start timer // This loop uses Register-Speed Integer Math for i = 1 to iterations balance = balance + micro_fee next i print "--- Results ---" print sprintf$("Final Balance: %m", balance) // %m = money format with commas print "Time Elapsed: "; _elapsed; " seconds" // Sheerpower remains exact. if balance = 105_000_000_000_100 then print "Verification: PERFECT. Zero drift after 10 million operations." else print "Verification: FAILED. Precision was lost:"; balance end if
The output:
Processing 10 million Transactions... --- Results --- Final Balance: 105,000,000,000,100 Time Elapsed: 0.218 seconds Verification: PERFECT. Zero drift after 10 million operations.

In IEEE doubles the same loop ends at exactly 105,000,000,000,000: at that size neighboring doubles are 0.015625 apart, so each 0.00001 added is rounded away and the whole $100 is lost.


Summary

Sheerpower's REAL type is internally managed as multiple 64-bit integers. By fixing the decimal scale at 16 digits, the architecture provides computational determinism for base-10 fractions, eliminating the representation drift and epsilon comparison requirements built into binary floating-point (IEEE 754) math.

Decimal types like C# decimal and IEEE 754-2008 _Decimal128 support flexible scaling. This makes them well suited for scientific and general-purpose use.

Sheerpower takes a different approach. It uses a split-integer design that targets high-volume transactional logic.

Instead of dynamic scaling, it uses a fixed decimal scale. This allows many operations to map directly to native 64-bit integer math.

As a result, common financial calculations can run very quickly. In many cases, operations complete in just a few CPU cycles.


Hide Description

    

       


      

Enter or modify the code below, and then click on RUN

Looking for the full power of Sheerpower?
Check out the Sheerpower website. Free to download. Free to use.