The rounding API of Math387 consolidates Delphi code, avoids ambiguity with a clearly defined behaviour for every function, and it improves on the performance:
- No ambiguity. Each function has one clearly defined behaviour: its name states the rounding rule, the type of the result and the precision, and the rule is written into the instruction, so the result does not depend on the rounding mode the processor happens to be in.
- Better performance. A line pays only for the conversion it asks for. A rounded value stays a floating-point number, with no round trip through an integer. An integer result has the width the code states,
IntegerorInt64. Single precision is rounded as single by thefforms, which saves a conversion todoubleand back on every call. - One behaviour across languages.
Round,Trunc,CeilandFloorreturn the rounded value, as their counterparts do in C, C++ and C#, andRoundbreaks ties to even, asrintdoes in C andMath.Roundin C#. A line carried from one language to another gives the same result.
Existing code that relied on the integer System.Round returned needs a small edit, and the compiler shows every line of it.
MtxVec 6.4 gives Round and Trunc the convention that Ceil and Floor have always had in MtxVec: the function named after the rounding returns the rounded value, and the integer result has a name of its own. Math387 now declares all four in the same pattern:
| Rounding | Rounded value | 32-bit integer | 64-bit integer |
|---|---|---|---|
| to nearest, ties to even | Round(x) | RoundToInt(x) | RoundToInt64(x) |
| towards zero | Trunc(x) | TruncToInt(x) | TruncToInt64(x) |
| towards plus infinity | Ceil(x) | CeilToInt(x) | CeilToInt64(x) |
| towards minus infinity | Floor(x) | FloorToInt(x) | FloorToInt64(x) |
Single precision has the same set with an f suffix: Roundf, Truncf, Ceilf, Floorf, and RoundToIntf, RoundToInt64f, TruncToIntf, TruncToInt64f, CeilToIntf, CeilToInt64f, FloorToIntf, FloorToInt64f.
One name, one meaning
The name says what comes back. Round gives a rounded number, RoundToInt gives an integer. A line such as y := Round(x) reads as what it does, with no conversion hidden in it, and a reader never has to look up which Round the unit is calling to know the type of the result.
The same meaning in Delphi, C, C++ and C#
The value forms and the f suffix are the convention of the C library and of .NET, so a name means the same thing in every language MtxVec is used from:
| Math387 | C and C++ | C# |
|---|---|---|
Round(x), Roundf(x) | rint(x), rintf(x) | Math.Round(x), MathF.Round(x) |
Trunc(x), Truncf(x) | trunc(x), truncf(x) | Math.Truncate(x), MathF.Truncate(x) |
Ceil(x), Ceilf(x) | ceil(x), ceilf(x) | Math.Ceiling(x), MathF.Ceiling(x) |
Floor(x), Floorf(x) | floor(x), floorf(x) | Math.Floor(x), MathF.Floor(x) |
RoundToInt64(x), RoundToInt64f(x) | llrint(x), llrintf(x) | Convert.ToInt64(x) |
Round, rint, llrint, Math.Round and Convert.ToInt64 all break ties to even (rint and llrint under the default rounding mode), so a line carried from one language to another gives the same result. On Linux, Math387 binds its names directly to the C functions in the table, and in .NET Round and Roundf call Math.Round and MathF.Round. The one C function with another rule is round, which breaks ties away from zero; the C counterpart of Round is rint.
Exact for every input
An integer cannot hold every value a floating-point number can. The value forms can: Round(1e300) is 1e300, an infinity stays an infinity, a NaN stays a NaN, and the sign of a zero survives, so Round(-0.25) is minus zero. Rounding becomes an operation on numbers again, with no range to fall out of.
One instruction, its rule written into it
Each rounding is one instruction: ROUNDSD for a double and ROUNDSS for a single, in mode 8 (to nearest, ties to even) or mode 0Bh (towards zero). The rule is part of the instruction, so the result is the same whatever rounding mode other code has left set in the processor. System.Round rounded by the mode currently set, the MXCSR register on Win64 and the x87 control word on Win32, and on Win32 System.Trunc saved the control word, loaded a truncating one and restored it again on every call. TruncToInt on Win32 is a single CVTTSD2SI.
No conversion the code does not ask for
The new names say which result a line needs, and the compiler then emits no conversion for anything else. The code Delphi 13 generates for each form shows three savings; the comments list the rounding and conversion instructions of each line.
1. The rounded value stays a floating-point number
System.Round returns an Int64, so y := Round(x) into a double converted the value to an integer and back on every call. Math387.Round is the rounding alone, and the value never leaves floating point, which is also why its range is the full range of a double:
var x, y: double;
y := Round(x); // MtxVec 6.4: ROUNDSD
// the same line with System.Round, Win64: CVTSD2SI, CVTSI2SD
// the same line with System.Round, Win32: FISTP to memory, FILD from memory2. The integer has the width the code asks for
System.Round always returned an Int64, also when the result went into an Integer. In 32-bit code a 64-bit integer cannot be converted out of an SSE register, so on Win32 every call went through the x87 unit and memory. RoundToInt returns an Integer and stays in registers; RoundToInt64 is the form for a value that needs 64 bits:
var x: double;
var i: integer;
var i64: Int64;
i := RoundToInt(x); // Win32: ROUNDSD, CVTTSD2SI, in registers
i64 := RoundToInt64(x); // Win32: ROUNDSD, then FLD and FISTP through memoryOn Win64 both widths are one CVTTSD2SI after the rounding.
3. A single is rounded as a single
Round takes a double. Called with a single, it converts the argument to double before every call, and when the result goes back into a single, converts it again after the call. The f forms round the single itself:
var s, t: single;
var k: integer;
t := Round(s); // Win64: CVTSS2SD, ROUNDSD, CVTSD2SS
t := Roundf(s); // Win64: ROUNDSS
k := RoundToInt(s); // Win64: CVTSS2SD, ROUNDSD, CVTTSD2SI
k := RoundToIntf(s); // Win64: ROUNDSS, CVTTSS2SIOn Win32 the same conversions pass through the x87 unit and memory. Truncf, Ceilf, Floorf and every ToIntf and ToInt64f form take a single in the same way, so naming the precision saves the conversion on every call.
The compiler shows every line to edit
Code written for the integer that System.Round returned now meets a floating-point value, and Delphi never converts a floating-point value to an integer on its own. Every place that needs the integer is therefore a compile error, reported at the line where it is, before anything runs. Compiled with Delphi 13 against MtxVec 6.4:
uses Math387;
var x: double;
var i, k: integer;
var i64: Int64;
var a: array[0..9] of integer;
var s: string;
i := Round(x); // E2010 Incompatible types: 'Integer' and 'Double'
i64 := Trunc(x); // E2010 Incompatible types: 'Int64' and 'Double'
i := a[Round(x)]; // E2010 Incompatible types: 'Integer' and 'Double'
for k := 0 to Round(x) do ... // E2010 Incompatible types: 'Integer' and 'Double'
i := Round(x) div 2; // E2015 Operator not applicable to this operand type
s := IntToStr(Round(x)); // E2250 There is no overloaded version of 'IntToStr' that can...
Inc(i, Trunc(x)); // E2008 Incompatible types
case Round(x) of ... // E2001 Ordinal type requiredEach one is fixed by naming the integer:
i := RoundToInt(x);
i64 := TruncToInt64(x);
i := a[RoundToInt(x)];
for k := 0 to RoundToInt(x) do ...
s := IntToStr(RoundToInt(x));RoundToInt, RoundToInt64 and TruncToInt return exactly what System.Round and System.Trunc returned, ties to even included: 0.5 rounds to 0, 1.5 and 2.5 to 2, -2.5 to -2. The edit changes the name, never the result.
Lines that use the rounded value as a number compile as they are and return the same value as before, now without the trip through an integer.
Format, Write and Str
Three routines accept an argument of any type, so the compiler has no integer to insist on there:
Format('%d', [Round(x)])compiles, and raisesEConvertErrorwith the message Format '%d' invalid or incompatible with argument the first time it runs.Write(Round(x)),Writeln(Round(x))andStr(Round(x), s)compile and run, and print the value in floating-point form,3.00000000000000E+0000where they printed3.
In both, the integer form is the one to write, Format('%d', [RoundToInt(x)]), Writeln(RoundToInt(x)), and a search for Round( and Trunc( inside Format, Write, Writeln and Str calls finds every such line ahead of time.
What stays as it is
System.Round(x)andSystem.Trunc(x), written with the unit name, still returnInt64.Math387declaresAbsandSqrfordouble,IntegerandInt64, soAbs(i)on an integer still returns an integer;AbsfandSqrfare the single-precision forms.CeilandFloorreturn the rounded value, as they always have in MtxVec.