Skip to main content

Round and Trunc in MtxVec 6.4

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, Integer or Int64. Single precision is rounded as single by the f forms, which saves a conversion to double and back on every call.
  • One behaviour across languages. Round, Trunc, Ceil and Floor return the rounded value, as their counterparts do in C, C++ and C#, and Round breaks ties to even, as rint does in C and Math.Round in 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:

RoundingRounded value32-bit integer64-bit integer
to nearest, ties to evenRound(x)RoundToInt(x)RoundToInt64(x)
towards zeroTrunc(x)TruncToInt(x)TruncToInt64(x)
towards plus infinityCeil(x)CeilToInt(x)CeilToInt64(x)
towards minus infinityFloor(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:

Math387C 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 memory

2. 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 memory

On 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, CVTTSS2SI

On 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 required

Each 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 raises EConvertError with the message Format '%d' invalid or incompatible with argument the first time it runs.
  • Write(Round(x)), Writeln(Round(x)) and Str(Round(x), s) compile and run, and print the value in floating-point form, 3.00000000000000E+0000 where they printed 3.

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) and System.Trunc(x), written with the unit name, still return Int64.
  • Math387 declares Abs and Sqr for double, Integer and Int64, so Abs(i) on an integer still returns an integer; Absf and Sqrf are the single-precision forms.
  • Ceil and Floor return the rounded value, as they always have in MtxVec.