|
Internals: REAL and String Data Types |
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:
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 an exact decimal numeric type designed for money,
measurements, quantities, and other calculations where predictable
decimal behavior matters. It does not use binary floating-point, so
ordinary decimal values such as 0.1 are represented exactly.
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 and do not
accumulate the rounding drift associated with binary floating-point.
Values produced by these operations can therefore be compared reliably
using =, without epsilon tolerances.
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:
Output:
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:
Output:
Because sqrt(2) has an infinite decimal expansion, REAL retains as many
significant decimal digits as possible. 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.
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.)
Output:
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:
Output:
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:
Output:
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.
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:
Output:
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:
Output:
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:
Output:
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:
Output:
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):
Output:
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:
Output:
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:
_inf + 1 is _inf_inf - _inf is _nan1 / _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.
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.
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.
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.
Output:
REAL is intended for accurate, predictable decimal calculations across both ordinary business values and an exceptionally large dynamic range.
Sheerpower stores REAL numbers using multiple 64-bit integers. In its normal fixed-point representation, a REAL consists of:
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.
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.
The illustration below shows how a REAL value is stored:
64-bit integers.
13
5990000000000000
Here's a classic example of floating-point failure in other languages:
In C, JavaScript, Python, or Java, this returns:
But in Sheerpower, the output is:
Sheerpower gives you exact representation up to 16 fractional decimal places.
When an ordinary decimal result cannot be represented exactly within the
available precision, REAL uses half-to-even rounding.
Divide with divr(a, b) instead and a quotient that does not terminate is
kept as an exact fraction (Exact Fractions, above); the / operator always rounds.
divr(a, b) divides and keeps the
exact fraction when the quotient does not terminate —
divr(1, 3), divr(100, 3), divr(total, count),
divr(0.05, 12) (1/240). The fraction is carried exactly through
+ - * / and whole-number powers among whole numbers of any size, other fractions
and short decimals (nine places or fewer, such as 0.25) while its denominator is
at most 4,294,967,295 (232 - 1), so divr(1, 3) * 3 is 1
and divr(1, 3) + 0.25 is 7/12.
The / operator never builds one: 1 / 3 is the
sixteen-place decimal, and 1 / 3 * 3 is 0.9999999999999999. Beside
a long decimal (such as 0.3333333333333333), and at every print, a fraction is the
decimal it rounds to, and a result smaller than 0.00000001 is the 64-digit
*Sci* decimal the divide gives, not 0. The full story, with the functions
fraction$(), fraction(), numerator(),
denominator() and decimal(), is on the Exact
Fractions page (Internals: REAL and String Data Types).
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. 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.
Output:
Output:
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.
Output:
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.
| Operation | Sheerpower REAL | Java BigDecimal | Python Decimal | vs Java | vs Python |
|---|---|---|---|---|---|
| Addition | 3-85 cycles | 150-250 cycles | 200-400 cycles | 50-80× | 65-130× |
| Subtraction | 3-85 cycles | 150-250 cycles | 200-400 cycles | 50-80× | 65-130× |
| Multiplication | 150-200 cycles | 400-900 cycles | 800-1500 cycles | 2.5-6× | 4-10× |
| Division | 300-500 cycles | 1500-3500 cycles | 2500-6000 cycles | 3-12× | 5-20× |
| Operation | Sheerpower REAL | Java BigDecimal | Python Decimal | vs Java | vs Python |
|---|---|---|---|---|---|
| Addition | 1-2 cycles | 50-60 cycles | 200-250 cycles | 30-50× | 125-200× |
| Subtraction | 1-2 cycles | 50-60 cycles | 200-250 cycles | 30-50× | 125-200× |
| Multiplication | 6-10 cycles | 80-120 cycles | 400-500 cycles | 10-15× | 50-65× |
| Division (constant) | 25-30 cycles | 200-300 cycles | 1500-2000 cycles | 8-10× | 60-70× |
| Division (cached) | 25-30 cycles | 200-300 cycles | 1500-2000 cycles | 8-10× | 60-70× |
| Division (uncached) | 130-150 cycles | 200-300 cycles | 1500-2000 cycles | 1.5-2× | 12-13× |
/100 for percentage).
| Operation | Sheerpower REAL | Java BigDecimal | Python Decimal | vs Java | vs Python |
|---|---|---|---|---|---|
| Addition | 1 cycle | 50-60 cycles | 200-250 cycles | 50-60× | 200-250× |
| Subtraction | 1 cycle | 50-60 cycles | 200-250 cycles | 50-60× | 200-250× |
| Multiplication | 3 cycles | 60-70 cycles | 350-400 cycles | 20-23× | 115-133× |
| Division (constant) | 5 cycles | 110-130 cycles | 1200-1500 cycles | 22-26× | 240-300× |
| Division (cached) | 5 cycles | 110-130 cycles | 1200-1500 cycles | 22-26× | 240-300× |
| Division (uncached) | 40 cycles | 110-130 cycles | 1200-1500 cycles | 2.75-3.25× | 30-37× |
long.
| Implementation | Cycles per Transaction | Time on a Modern PC | Speedup |
|---|---|---|---|
| Sheerpower REAL | 1+1+10+5+3 = 20 cycles | 10 nanoseconds | Baseline |
| Java BigDecimal | 50+50+100+110+150 = 460 cycles | 230 nanoseconds | 23× slower |
| Python Decimal | 200+200+450+1200+250 = 2,300 cycles | 1,150 nanoseconds | 115× slower |
Real-world impact: Processing a million invoices takes 1 second in Sheerpower vs 23 seconds in Java vs 115 seconds (almost 2 minutes) in Python.
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 why mainstream languages committed early to IEEE 754 binary floating-point (standardized in 1985) and to heap-allocated arbitrary-precision libraries like BigDecimal. 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.
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.
In this advanced discussion, we delve into Sheerpower's memory management and optimization strategies for handling strings efficiently.
Sheerpower manages strings by reusing memory buffers, which significantly reduces the overhead of memory allocations and deallocations. For example, if a string holds "The rain in Spain" and is later changed to "Enjoy life", Sheerpower repurposes the existing buffer. In most applications, over 99% of string buffers are reused.
This approach avoids the frequent memory operations that cause
performance bottlenecks in other languages.
All strings are over-allocated and virtually mutable, making assignments O(1)
operations with no allocation/deallocation cycles.
In many languages, every string modification requires a new memory allocation, and the old memory must be deallocated. This cycle introduces overhead, especially in loops.
num$. The buffer is reused in each iteration, a stark
contrast to languages that could perform 10,000 allocations and
deallocations, slowing down performance.
Based on research showing 85% of business strings are under 128 bytes, Sheerpower pre-allocates thousands of fixed-size buffers on startup.
Sheerpower assigns every unique string value a 64-bit sequential String ID (SID). When a string variable is first created, the runtime generates a new SID for its content. When another variable is assigned the same content—either by direct assignment or by copying—the SID from the source descriptor is copied into the destination descriptor along with the data. In other words, the SID travels with the data.
The SID is stored directly inside the string's descriptor, alongside the string's length, the pointer to its character buffer, and the buffer's current allocation size. Because the SID lives inside the descriptor rather than in a centralized lookup table, Sheerpower avoids global SID-table lookups and enables several powerful runtime optimizations:
String literal assignments cost almost nothing, as the descriptor simply points to the literal in the compiled code. A copy is only triggered upon modification.
A boolean array — dim flags?(n), or the hidden result
of a comparison such as scores >= 60 — is stored as a
bit array: eight flags a byte, so a hundred million flags
take 12.5 MB and a billion 125 MB. The array descriptor keeps
its element length at 1 and the element address is an ordinal;
the two accessors that read and write a boolean element decode it as byte
k / 8, bit k mod 8, so every path that walks an
array by element — subscripts, fill, print,
filter(), sort, the statistics — works
unchanged. A store accepts only 0 or 1 (NOT0OR1 otherwise),
so a bit can never hold anything a bit cannot hold.
The speed comes from the kernels that never touch a single bit: when
every operand of a mask expression is packed, and,
or, not and the comparison masks run a
word at a time, 64 flags a step; stats$sum(mask)
is a population count over the words; sort of a boolean array
is a population count followed by a fill. Measured: a billion flags
counted in 0.11 s, a? and not b? over a billion in
0.3 s, a boolean array sorted at 3 ns an element. Comparisons whose
operands are integer arrays take an all-integer fast path that writes the
packed mask directly; typeof$() reports Dtype:Boolean,
*Packed* for any such array.
Sheerpower uses an internal hinting mechanism, using SIDs to remember recent operations and cache results. This enhances efficiency by avoiding redundant processing.
Concatenation Hinting: A cache-sized hint array remembers recent concatenation operations by SID combinations. For repetitive tasks like building SQL queries, subsequent operations become simple cache lookups.
Function Parameter Parsing Cache: Functions like
getword$() and geodistance() automatically cache
parsing results by string content.
If you call getword$(bible$, 1000) and then
getword$(bible$, 1001), Sheerpower uses the hint to
quickly find the next word without re-reading from the start.
These transparent optimizations provide the most benefit for typical business application patterns:
Sheerpower's memory management shines in applications where performance is critical, such as real-time data analysis, and large-scale batch processing systems.
By understanding these advanced techniques, you can harness Sheerpower's capabilities to build highly efficient, scalable, and robust applications that handle strings with high performance.
|
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. |