cint

an orbit in whole numbers, and the same bytes after a hundred million steps

Harriett Little. Published 2026-10-06.
The program links on this page load into the one Run box. Edit it and run it again.

Two simulations that propagate the same orbit from the same state with the same integrator should arrive at the same place, and in floating point they do not, quite. The arithmetic is specified to the bit, but what a compiler makes of an expression is not: one fuses a multiply and an add into a single rounding and another does not, one library's square root differs from another's in the last place, and after enough steps the two trajectories are different numbers. In a distributed simulation, where federates on different machines may each carry a copy of a state, that is a disagreement nobody chose. This page propagates an orbit in checked integer arithmetic, where there is only one answer, and shows the same bytes from four compilers on three machines after a hundred million steps, beside the same integrator in double from the same compilers, which do not agree. It also shows what that costs: the integer program is twenty-five to forty-four times slower than the double one.

two kinds of error

They are easy to run together and this page keeps them apart. Truncation error is the integrator's: a fixed step of one second with a first-order method puts the vessel a few kilometers from where the exact solution would, and it does so identically on every machine, because it is a property of the method and not of the hardware. Reproducibility is the other thing: whether two machines running the same method get the same bits. Floating point fails the second even when both machines pass the first by the same margin. Integer arithmetic has only the first. The orbit below is not more accurate than its floating-point twin, and the page does not say it is. It is the same everywhere.

the program

Earth, a circular orbit at 6,778.137 km, one second per step, semi-implicit Euler: kick the velocity by gravity, then drift the position by the new velocity. Position is in centimeters, velocity in micrometers per second, and the part of a step that is less than a centimeter is carried in a remainder of its own, so nothing is dropped and the position is exact to the micrometer. Gravity is GM / r², with the square root an integer square root and the two divisions a muldiv each, which keeps the product wide and rounds once, by name. The radius squared in centimeters is about 4.6 x 1017, inside I64 with room to spare, and that is the whole reason for the units: they were chosen so that every intermediate fits, and if one did not the program would stop and say which. A digest of every state is folded along the way; it wraps on purpose, marked with the wrapping operators, because it is a fingerprint and not a quantity.

orbit.ci   Run   the first Run loads Python, about 12 MB, once

via cint_ref (reference written in .py)
stdout

    outcome
    

  

what the output shows

One period, 5,554 steps. The vessel comes back to within 2.85 kilometers of its start, which is the truncation error of a one-second first-order step and the fact that 5,554 seconds is not quite the period, and the radius stays within 3.9 kilometers of its starting value through the whole orbit, the small oscillation this integrator is known for. Then the digest. Run it twice and the digest is the same. Run it in the compiler and the digest is the same. Change the step or the starting speed and it changes, as it should.

the radius over the first orbit, inside the range of all hundred million steps 6,774 6,776 6,778 6,780 6,782 radius, km 0 1,000 2,000 3,000 4,000 5,000 seconds into the first orbit shaded: the range over all 10⁸ steps 0 s: 6,778.137 km 50 s: 6,777.920 km 100 s: 6,777.704 km 150 s: 6,777.489 km 200 s: 6,777.277 km 250 s: 6,777.067 km 300 s: 6,776.861 km 350 s: 6,776.658 km 400 s: 6,776.461 km 450 s: 6,776.268 km 500 s: 6,776.082 km 550 s: 6,775.903 km 600 s: 6,775.730 km 650 s: 6,775.565 km 700 s: 6,775.409 km 750 s: 6,775.261 km 800 s: 6,775.122 km 850 s: 6,774.994 km 900 s: 6,774.875 km 950 s: 6,774.766 km 1,000 s: 6,774.669 km 1,050 s: 6,774.582 km 1,100 s: 6,774.507 km 1,150 s: 6,774.444 km 1,200 s: 6,774.392 km 1,250 s: 6,774.353 km 1,300 s: 6,774.325 km 1,350 s: 6,774.310 km 1,400 s: 6,774.307 km 1,450 s: 6,774.316 km 1,500 s: 6,774.338 km 1,550 s: 6,774.372 km 1,600 s: 6,774.418 km 1,650 s: 6,774.475 km 1,700 s: 6,774.545 km 1,750 s: 6,774.626 km 1,800 s: 6,774.718 km 1,850 s: 6,774.821 km 1,900 s: 6,774.935 km 1,950 s: 6,775.059 km 2,000 s: 6,775.193 km 2,050 s: 6,775.337 km 2,100 s: 6,775.489 km 2,150 s: 6,775.650 km 2,200 s: 6,775.819 km 2,250 s: 6,775.995 km 2,300 s: 6,776.178 km 2,350 s: 6,776.368 km 2,400 s: 6,776.563 km 2,450 s: 6,776.763 km 2,500 s: 6,776.968 km 2,550 s: 6,777.176 km 2,600 s: 6,777.387 km 2,650 s: 6,777.601 km 2,700 s: 6,777.817 km 2,750 s: 6,778.033 km 2,800 s: 6,778.250 km 2,850 s: 6,778.466 km 2,900 s: 6,778.682 km 2,950 s: 6,778.896 km 3,000 s: 6,779.107 km 3,050 s: 6,779.315 km 3,100 s: 6,779.520 km 3,150 s: 6,779.720 km 3,200 s: 6,779.915 km 3,250 s: 6,780.104 km 3,300 s: 6,780.287 km 3,350 s: 6,780.463 km 3,400 s: 6,780.632 km 3,450 s: 6,780.793 km 3,500 s: 6,780.945 km 3,550 s: 6,781.088 km 3,600 s: 6,781.222 km 3,650 s: 6,781.346 km 3,700 s: 6,781.460 km 3,750 s: 6,781.563 km 3,800 s: 6,781.655 km 3,850 s: 6,781.737 km 3,900 s: 6,781.806 km 3,950 s: 6,781.864 km 4,000 s: 6,781.910 km 4,050 s: 6,781.944 km 4,100 s: 6,781.965 km 4,150 s: 6,781.975 km 4,200 s: 6,781.972 km 4,250 s: 6,781.957 km 4,300 s: 6,781.930 km 4,350 s: 6,781.891 km 4,400 s: 6,781.839 km 4,450 s: 6,781.776 km 4,500 s: 6,781.702 km 4,550 s: 6,781.616 km 4,600 s: 6,781.518 km 4,650 s: 6,781.410 km 4,700 s: 6,781.292 km 4,750 s: 6,781.163 km 4,800 s: 6,781.025 km 4,850 s: 6,780.878 km 4,900 s: 6,780.722 km 4,950 s: 6,780.557 km 5,000 s: 6,780.385 km 5,050 s: 6,780.206 km 5,100 s: 6,780.020 km 5,150 s: 6,779.828 km 5,200 s: 6,779.631 km 5,250 s: 6,779.429 km 5,300 s: 6,779.222 km 5,350 s: 6,779.013 km 5,400 s: 6,778.800 km 5,450 s: 6,778.586 km 5,500 s: 6,778.370 km 5,550 s: 6,778.153 km
The radius through the first orbit, every tenth step, from the program run in cint_ref with one print line added. It swings between 6,774.307 and 6,781.975 km once per orbit, the oscillation of the semi-implicit step. The shaded band is the range over all 10⁸ steps of the compiled run, 6,774.295 to 6,781.985 km: after about 18,000 orbits it reaches 11.6 m lower and 9.7 m higher than the first orbit did.
data
secondradius, km
06,778.137
1006,777.704
2006,777.277
3006,776.861
4006,776.461
5006,776.082
6006,775.730
7006,775.409
8006,775.122
9006,774.875
1,0006,774.669
1,1006,774.507
1,2006,774.392
1,3006,774.325
1,4006,774.307
1,5006,774.338
1,6006,774.418
1,7006,774.545
1,8006,774.718
1,9006,774.935
2,0006,775.193
2,1006,775.489
2,2006,775.819
2,3006,776.178
2,4006,776.563
2,5006,776.968
2,6006,777.387
2,7006,777.817
2,8006,778.250
2,9006,778.682
3,0006,779.107
3,1006,779.520
3,2006,779.915
3,3006,780.287
3,4006,780.632
3,5006,780.945
3,6006,781.222
3,7006,781.460
3,8006,781.655
3,9006,781.806
4,0006,781.910
4,1006,781.965
4,2006,781.972
4,3006,781.930
4,4006,781.839
4,5006,781.702
4,6006,781.518
4,7006,781.292
4,8006,781.025
4,9006,780.722
5,0006,780.385
5,1006,780.020
5,2006,779.631
5,3006,779.222
5,4006,778.800
5,5006,778.370

a hundred million steps

The same program with the step count raised to 100,000,000, about 18,000 orbits, emitted to C by cintc once and built with four compilers on three machines, an x86-64 Linux host, an x86-64 Windows host and an arm64 Mac, each at -O2 (/O2 for MSVC):

buildmachinefinal position, cmstate digest
clang 18.1.3x86-64 Linux24,708,944   676,979,33715231276719831660197
gcc 13.3.0x86-64 Linux24,708,944   676,979,33715231276719831660197
MSVC 19.44x86-64 Windows 1124,708,944   676,979,33715231276719831660197
Apple Clang 21.0.0arm64 macOS 2724,708,944   676,979,33715231276719831660197

Every byte of output is identical between the four builds, the four lines and their SHA-256 alike, 68d2475161b0946c69e0373f6dbd50768ac17848e30c848ed6aa16df96608a7a. The radius stayed between 6,774.295 and 6,781.985 kilometers for the whole three years of simulated time. How long each build took, beside the same loop in double, is in what the checking costs, below.

the same integrator in double

Thirty lines of C, the same state, the same step, the same kick and drift, in meters and meters per second, with the same digest folded over the bit patterns of the state. Built four ways on the same machine, the same hundred million steps:

buildfused multiply-addtimefinal x, mstate digest
clang -O2 -ffp-contract=offoff1.5 s228619.662865483527140356435394844241
clang -O2 -ffp-contract=fast -mfmaallowed1.6 s228619.662865483527140356435394844241
gcc -O2 -ffp-contract=offoff1.5 s228619.662865483527140356435394844241
gcc -O2 -mfmagcc's default1.4 s228618.73339447728295821013259388869

Three of the four agree and the fourth does not. gcc, at its default contraction setting with fused multiply-add available, puts the vessel 93 centimeters from where the other three put it, after the same hundred million steps of the same source on the same processor, and its digest shares nothing with theirs. Nothing is wrong with any of the four. Each is a correct compilation of a program whose result the language leaves to the compiler. That is the disagreement the integer version does not have.

distance between the gcc build with fused multiply-add and the other three double builds, by step 1 nm 1 µm 1 mm 1 m 10² 10³ 10⁴ 10⁵ 10⁶ 10⁷ 10⁸ steps of one second 186: first bit apart, in vx 3,103: last step equal 0.93 m after 10⁸ steps step 3,981: 6.98e-10 m step 4,467: 1.4e-09 m step 5,012: 1.32e-09 m step 5,623: 3.26e-09 m step 6,310: 4.66e-09 m step 7,079: 4.66e-09 m step 7,943: 7.09e-09 m step 8,913: 2.06e-08 m step 10,000: 4.68e-08 m step 11,220: 5.76e-08 m step 12,589: 9.05e-08 m step 14,125: 2.94e-07 m step 15,849: 6.1e-07 m step 17,783: 9.46e-07 m step 19,953: 1.52e-06 m step 22,387: 2.29e-06 m step 25,119: 3.22e-06 m step 28,184: 3.97e-06 m step 31,623: 4.31e-06 m step 35,481: 5.5e-06 m step 39,811: 6.61e-06 m step 44,668: 7.88e-06 m step 50,119: 9.7e-06 m step 56,234: 1.25e-05 m step 63,096: 1.54e-05 m step 70,795: 1.81e-05 m step 79,433: 2.39e-05 m step 89,125: 2.97e-05 m step 100,000: 3.58e-05 m step 112,202: 4.87e-05 m step 125,893: 6.5e-05 m step 141,254: 8.66e-05 m step 158,489: 0.000112 m step 177,828: 0.000142 m step 199,526: 0.000176 m step 223,872: 0.000219 m step 251,189: 0.000273 m step 281,838: 0.000336 m step 316,228: 0.000411 m step 354,813: 0.000505 m step 398,107: 0.00061 m step 446,684: 0.000723 m step 501,187: 0.000828 m step 562,341: 0.000887 m step 630,957: 0.000912 m step 707,946: 0.000897 m step 794,328: 0.000795 m step 891,251: 0.000668 m step 1,000,000: 0.00046 m step 1,122,018: 0.000219 m step 1,258,925: 4.91e-05 m step 1,412,538: 0.000374 m step 1,584,893: 0.000802 m step 1,778,279: 0.00123 m step 1,995,262: 0.00168 m step 2,238,721: 0.00195 m step 2,511,886: 0.00222 m step 2,818,383: 0.00294 m step 3,162,278: 0.00405 m step 3,548,134: 0.00552 m step 3,981,072: 0.00694 m step 4,466,836: 0.00793 m step 5,011,872: 0.00815 m step 5,623,413: 0.00861 m step 6,309,573: 0.00829 m step 7,079,458: 0.00712 m step 7,943,282: 0.00613 m step 8,912,509: 0.00703 m step 10,000,000: 0.0119 m step 11,220,185: 0.0158 m step 12,589,254: 0.0227 m step 14,125,375: 0.0346 m step 15,848,932: 0.047 m step 17,782,794: 0.0585 m step 19,952,623: 0.0714 m step 22,387,211: 0.0878 m step 25,118,864: 0.0941 m step 28,183,829: 0.106 m step 31,622,777: 0.13 m step 35,481,339: 0.165 m step 39,810,717: 0.221 m step 44,668,359: 0.272 m step 50,118,723: 0.305 m step 56,234,133: 0.33 m step 63,095,734: 0.393 m step 70,794,578: 0.509 m step 79,432,823: 0.631 m step 89,125,094: 0.792 m step 100,000,000: 0.93 m
The gcc build with fused multiply-add against the other three double builds of the table above: how far apart their positions are, at 150 checkpoints between one step and a hundred million. The states first differ at step 186, in the last bit of one velocity component, agree again on and off, and after step 3,103 never agree again. The integer builds agree at every step, so they have no line. From orbit_double_checkpoints.c, built the four ways of the table.
data
stepdistance, m
3,9816.98e-10
4,4671.4e-09
5,0121.32e-09
5,6233.26e-09
6,3104.66e-09
7,0794.66e-09
7,9437.09e-09
8,9132.06e-08
10,0004.68e-08
11,2205.76e-08
12,5899.05e-08
14,1252.94e-07
15,8496.1e-07
17,7839.46e-07
19,9531.52e-06
22,3872.29e-06
25,1193.22e-06
28,1843.97e-06
31,6234.31e-06
35,4815.5e-06
39,8116.61e-06
44,6687.88e-06
50,1199.7e-06
56,2341.25e-05
63,0961.54e-05
70,7951.81e-05
79,4332.39e-05
89,1252.97e-05
100,0003.58e-05
112,2024.87e-05
125,8936.5e-05
141,2548.66e-05
158,4890.000112
177,8280.000142
199,5260.000176
223,8720.000219
251,1890.000273
281,8380.000336
316,2280.000411
354,8130.000505
398,1070.00061
446,6840.000723
501,1870.000828
562,3410.000887
630,9570.000912
707,9460.000897
794,3280.000795
891,2510.000668
1,000,0000.00046
1,122,0180.000219
1,258,9254.91e-05
1,412,5380.000374
1,584,8930.000802
1,778,2790.00123
1,995,2620.00168
2,238,7210.00195
2,511,8860.00222
2,818,3830.00294
3,162,2780.00405
3,548,1340.00552
3,981,0720.00694
4,466,8360.00793
5,011,8720.00815
5,623,4130.00861
6,309,5730.00829
7,079,4580.00712
7,943,2820.00613
8,912,5090.00703
10,000,0000.0119
11,220,1850.0158
12,589,2540.0227
14,125,3750.0346
15,848,9320.047
17,782,7940.0585
19,952,6230.0714
22,387,2110.0878
25,118,8640.0941
28,183,8290.106
31,622,7770.13
35,481,3390.165
39,810,7170.221
44,668,3590.272
50,118,7230.305
56,234,1330.33
63,095,7340.393
70,794,5780.509
79,432,8230.631
89,125,0940.792
100,000,0000.93

The same thirty lines on the other two machines, and clang on the first once more with its vectorizer off:

buildmachinefinal x, mstate digest
Apple Clang -O2 -ffp-contract=offarm64 macOS228619.662865483527140356435394844241
Apple Clang -O2arm64 macOS228618.73339447728295821013259388869
Apple Clang -O2 -ffp-contract=fastarm64 macOS228618.702782635467708669496574730328
MSVC /O2 /fp:precisex86-64 Windows228619.662865483527140356435394844241
MSVC /O2 /fp:contractx86-64 Windows228619.662865483527140356435394844241
MSVC /O2 /fp:fastx86-64 Windows228619.571628213862618997089874377949
clang -O2 -ffp-contract=fast -mfma -fno-slp-vectorizex86-64 Linux228618.702782635467708669496574730328

On arm64 the fused multiply-add is always there, and Apple Clang's default build uses it: it lands on gcc's answer from the table above, to the bit. Allowed to fuse wherever it likes, it gives a third answer, and MSVC's fast mode a fourth. The clang build above that was allowed to fuse and still agreed with the unfused builds did so because clang packed x and y into one vector register and fused nothing there; with the vectorizer off it fuses, and gives the third answer too. Four answers from one source, every one a correct compilation, and the integer program gave one answer on all four compilers.

what the checking costs

The hundred-million-step program against the thirty lines of double, each built at -O2 (/O2 for MSVC) and run once, one after the other on the same machine, with the double built unfused (-ffp-contract=off, or /fp:precise for MSVC):

buildmachinechecked integerdoubleinteger / double
clang 18.1.3x86-64 Linux44.6 s1.8 s25
gcc 13.3.0x86-64 Linux49.8 s1.8 s28
MSVC 19.44x86-64 Windows 1161.2 s1.4 s44
Apple Clang 21.0.0arm64 macOS 2725.7 s0.9 s29

The double version is twenty-five to forty-four times faster, depending on the compiler. A step of the integer loop takes an integer square root and three multiply-divides with a wide product, and every plain operator in it is checked; a step of the double loop takes one hardware square root and three divisions. How the time splits between the square root, the wide products and the checks has not been measured, so the ratio is the price of the whole program, with the compiler that emitted the C on this site, and not of checking alone. Each time is a single run, so the double times here differ by a few tenths of a second from the first machine's times in the double table above, which came from other runs. It is stated here rather than left out.

same bytes from the reference and the compiler

The box below is the C that cintc emitted for the one-period program as shipped, 536 lines for 50 lines of cint. The compiled program and the reference write the same four lines, with stdout SHA-256 d2b4d07efa7dda4019e55138d3da0d41c0155233af360da18aeceafa3d0c76b1, and the test passes under both.

cintc (the cint compiler)


128 bits

orbit_wide.ci is the same orbit with the state in I128: position in micrometers in one number, no remainder to carry, and squares of about 4.6 x 1025 with about forty bits to spare. It is the shape the state would take in a simulator that wanted femtometer remainders and planetary distances in the same word. The reference runs it; the compiler does not accept I128 yet, so the lower box will say so when it is loaded, and the hundred-million-step result for this version waits on that.

what is not claimed

An earlier simulator by the same author, written in a different language, carried an orbit in integers with explicit units and rounding rules and recorded bit-identical state on x86-64 and AArch64 machines across 1.5 million per-tick digests, and published its state through the SpaceFOM. That work is cited below; this page is the same idea in cint, in fifty lines, with the arithmetic checked by the language rather than by the program.

The machines are one x86-64 Linux host, one x86-64 Windows host and one arm64 Mac, so the identity shown is across four compilers and two architectures, not a survey of hardware. Nothing here exchanged state over HLA or spoke to a federation; the paper's argument about distributed simulation is that two federates computing the same thing would agree, and it is made with three machines standing in for federates. The integrator is first-order with a one-second step and is not offered as an accurate propagator. There is no drag, no oblateness and no third body.

limits

The units were chosen for this orbit. A different radius or a different step moves the intermediates, and the first thing to do with the program is to check that they still fit, which the program itself will do by faulting if they do not. The floating-point comparison is one program on three machines with four compilers; it shows that the result depends on compilation choices, which is all it is asked to show, and the size of the difference, under a meter after three years, is not a measure of anything general. The digest is a fingerprint for comparing runs and is not a cryptographic hash.

references

cite as

Harriett Little. An orbit in whole numbers, and the same bytes after a hundred million steps. integerc.dev, 2026. https://integerc.dev/papers/orbit/

Harriett Little
October 2026