C Part II · Module 20 · DPI-C Type Mapping
Mapping real and Floating-Point Values
SystemVerilog's real maps to C's double and shortreal to float, and the mapping is the easy part — measured here, a double crosses and returns bit for bit exact. The folk belief that floating point loses precision at the boundary is wrong, and believing it sends you looking in the wrong place. The losses are in the literal before anything crosses, in arithmetic each side performs independently, and in the narrowing to float. The chapter ends on a measured finding: two tolerance checks that are algebraically identical disagree about a NaN, and the one people write is the one that reports nothing.
Advanced22 min readCPart IIVerificationDPI-C
Part II · Module 20 · Chapter 20.7 · Mapping real and Floating-Point Values
1. The Mapping Itself
From the LRM, two types and two mappings:
- SystemVerilog
real→ Cdouble - SystemVerilog
shortreal→ Cfloat
Measured on this machine:
sizeof(double)=8 sizeof(float)=4
DBL_MANT_DIG=53 FLT_MANT_DIG=24 (bits of significand, incl. implicit)
DBL_DIG=15 FLT_DIG=6 (decimal digits guaranteed to round-trip)Eight bytes to eight bytes for real, four to four for shortreal. Unlike Chapter 20.3's integer types there is no width to choose and no long to be wrong about, and unlike Chapter 20.4 there is no signedness question — a floating-point type has a sign bit and both languages agree on it.
So the mapping is the easiest in the module, and the chapter is about what the mapping does not settle.
2. What the C Standard Actually Promises
A precision point before the measurements, because this module has been careful about what is guaranteed and by whom.
The LRM describes real as a double-precision IEEE value. C is more cautious: it guarantees IEEE-754 semantics only when the implementation defines __STDC_IEC_559__, and measured on this machine:
__STDC_IEC_559__ defined: NOSo everything below — the bit patterns, the rounding, the behaviour of NaN — is implementation-defined in the strict ISO C sense, not guaranteed by the language. In practice this implementation is plainly IEEE binary64 and binary32; the measurements show it. But the classification is the honest one, and it matters for the same reason Chapter 16.6's did: "it works here" and "the standard requires it" are different claims, and a model that relies on the second when only the first is true is relying on something nobody promised.
Treat IEEE behaviour as a property of the toolchain you have, verifiable by the macro, rather than as a language guarantee.
3. The Crossing Is Exact
The folk belief is that floating point loses precision at the boundary. Measured, sending five values — including 0.1, 1/3, 1e300 and the smallest subnormal 5e-324 — out through their eight raw bytes and reading them back:
double round trip through 8 raw bytes, 5 values: exactNot close. Identical, bit for bit — which Chapter 14.5 made checkable, because an object's representation is just bytes and two doubles are equal as values exactly when those bytes agree.
A double is 8 bytes on both sides and nothing reinterprets them. There is no narrowing, no re-rounding, and no conversion. The real crossing costs nothing.
That is worth stating plainly because it redirects the search. A mismatch between a model and a design is not caused by the boundary, so the next three sections are where to look instead.
4. Where the Precision Actually Goes
First — the literal was never the number you wrote.
double 0.1 = 0x3FB999999999999A = 0.100000000000000005550.1 is not representable in binary at all, so the nearest double is stored instead, and it is a little larger. Nothing has crossed any boundary yet. The same thing happens to the SystemVerilog literal, in the same direction, to the same value — which is why the crossing agrees.
The consequence everyone has met:
0.1+0.2==0.3 : FALSE (0.1+0.2 = 0.30000000000000004441)Second — each side does its own arithmetic. Ten additions of 0.1:
ten additions of 0.1 = 0.99999999999999989
== 1.0 : FALSE
|x-1.0| = 1.11e-16And the order matters:
(0.1+0.2)+0.3 = 0.60000000000000009
0.1+(0.2+0.3) = 0.59999999999999998
equal: FALSEFloating-point addition is not associative. If the C model sums a payload in a different order from the design — or the compiler reassociates, which it may do under fast-math options — the two answers differ in the last bits, with no bug anywhere. This is the real source of model-versus-design floating-point mismatches, and it is arithmetic, not the boundary.
Third — shortreal narrows. 53 bits of significand into 24:
0.1 as double = 0.10000000000000001
0.1 as float = 0.10000000149011612
error introduced by the narrowing: 1.49e-09
1/3 double->float->double error: 9.93e-09Unlike the first two, this one is a property of the crossing — it happens because the two types are different widths, and it happens to nearly every value. If the design's data is genuinely 32-bit, shortreal is correct and the narrowing is not a loss. If the C model computes in double and hands back a shortreal, the answer is rounded on the way out.
5. Comparing a Model's Answer to a Design's
Given §4, == is not the comparison you want for computed floating-point values. The standard answers are a tolerance — and the choice between absolute and relative is not a matter of taste.
Measured at three magnitudes, with an absolute tolerance of 1e-9:
1 vs 1.0000000001 abs diff 1e-10 (abs<=1e-9: pass) rel diff 1e-10 (rel<=1e-9: pass)
1e+09 vs 1000000000 abs diff 0 (abs<=1e-9: pass) rel diff 0 (rel<=1e-9: pass)
1e-09 vs 2e-09 abs diff 1e-09 (abs<=1e-9: pass) rel diff 0.5 (rel<=1e-9: FAIL)Read the second and third rows.
At 1e9, perturbing the value by 1e-10 did not change it at all — the abs diff is exactly 0, because the perturbation is far smaller than the spacing between adjacent doubles at that magnitude. An absolute tolerance of 1e-9 is meaningless up there; it can never fail.
At 1e-9, the second value is twice the first, and an absolute tolerance of 1e-9 passes it. A 100% error reported as agreement.
So: an absolute tolerance only makes sense over a known, narrow range of magnitudes. Across magnitudes, use a relative tolerance — and keep an absolute floor for values near zero, where a relative comparison divides by something tiny.
6. Two Checks That Are Not the Same Check
Here is the finding, and it is the reason this chapter is longer than its mapping deserves.
These two tolerance checks are algebraically identical:
int form_A(double got, double exp, double tol) { return !(fabs(got-exp) <= tol); }
int form_B(double got, double exp, double tol) { return fabs(got-exp) > tol; }Measured, applied to a result that is a NaN — produced with nan(""), which is defined, rather than 0.0/0.0, which on an implementation that does not define __STDC_IEC_559__ is a domain error with undefined behaviour:
n == n : FALSE n != n : TRUE
two tolerance checks that look equivalent, applied to a NaN result:
form A !(fabs(got-exp) <= tol) reports mismatch: YES
form B (fabs(got-exp) > tol) reports mismatch: noAnd on ordinary values they agree:
the same two checks on a genuine 1e-16 difference:
form A reports mismatch: no form B reports mismatch: no
and on a genuine 1.0 difference:
form A reports mismatch: YES form B reports mismatch: YESEvery comparison involving a NaN is false — including >. So form B asks "is the difference greater than the tolerance?", gets false, and reports agreement. Form A asks "is the difference within tolerance?", gets false, and negates it into a mismatch.
The two forms differ on exactly one input, and that input is the one a checker exists to catch: the design or the model produced something that is not a number.
This is Chapter 18.2's failure again, in its smallest possible form. A checker that cannot report the worst case is a checker whose silence means less than it appears to — and the version people write, because it reads more naturally, is the broken one.
Two defences:
if (isnan(got) || isnan(exp)) { report("not a number"); return; }
if (!(fabs(got - exp) <= tol)) { report("outside tolerance"); return; }Check for NaN explicitly and first, then write the tolerance in the !(… <= …) form so a NaN that slips past the first check is still caught by the second.
7. Passing Floating-Point Values Deliberately
A short checklist, each item from a measurement above.
- Use
real/doubleunless the data is genuinely 32-bit. The crossing is free; the narrowing is not. - Carry integers as integers. A
longintthrough arealloses everything past 2⁵³. - Never
==on a computed value, and never a bare absolute tolerance across magnitudes. - Check
isnanfirst, then use!(diff <= tol). - Sum in the same order on both sides if you need the last bits to agree, and be aware the compiler may reassociate under fast-math options.
- Verify
__STDC_IEC_559__in your build if you are relying on IEEE semantics; here it is not defined.
8. The Misconception: The Boundary Loses Precision
"Floating point is lossy, so a
realloses a little precision when it crosses DPI. That is where the mismatch comes from."
Measured false. Five values, including the smallest subnormal, round-tripped exact. Eight bytes to eight bytes, nothing reinterprets them.
The belief is not harmless, because it is an explanation, and an explanation stops a search. An engineer who believes it sees a model and a design disagreeing in the fifteenth digit, attributes it to DPI, and adds a tolerance. The tolerance makes the symptom go away, and the actual cause — §4's summation order, or a genuine model bug of the same size — is never looked at.
The corrected position:
The
realcrossing is exact. Precision is lost in the literal before anything crosses, in each side's arithmetic independently, and inshortreal's narrowing — and whether you detect a real disagreement depends on the shape of your tolerance check, not on the boundary.
The one case where the boundary genuinely loses something is shortreal, where it is not a defect but the mapping doing what it says: measured, 1.49e-09 on 0.1, every time, by construction.
9. What You Should Be Able to Say Now
real→double,shortreal→float— no width to choose, no signedness question.- Measured:
double8 bytes / 53 significand bits / 15 decimal digits;float4 / 24 / 6. - IEEE semantics are not a C guarantee unless
__STDC_IEC_559__is defined — measured, it is not defined here, so the behaviour below is implementation-defined. - The
realcrossing is exact — five values round-tripped bit for bit, including5e-324and1e300. - Loss one, the literal:
0.1is stored as0x3FB999999999999A=0.10000000000000000555, before anything crosses. - Loss two, the arithmetic: ten additions of
0.1give0.99999999999999989, and addition is not associative —(0.1+0.2)+0.3 ≠ 0.1+(0.2+0.3), measured. - Loss three, the narrowing:
shortrealcosts 1.49e-09 on0.1by construction. - A
doubleholds integers exactly only to 2⁵³ —2⁵³+1 == 2⁵³is true, andINT64_MAXconverts to a value one larger than itself. Carry integers as integers. - Absolute tolerance fails at both ends: at
1e9a perturbation of1e-10vanishes entirely; at1e-9a tolerance of1e-9passes a 100% error. - The finding:
!(d <= tol)reports a NaN mismatch;d > toldoes not — measured, identical on every other input. Checkisnanfirst, then use the negated form.
10. Exercises
- Print the bit pattern of
0.1as adoubleand confirm0x3FB999999999999A. Then print0.1 + 0.2to 20 decimal places. - Round-trip five doubles through their raw bytes and confirm exactness. Include a subnormal.
- Narrow
0.1tofloatand back; confirm the error is1.49e-09. Repeat for1.0/3.0. - Show
2⁵³ + 1 == 2⁵³. Then convertINT64_MAXtodoubleand print it as an integer — which way did it round? - Implement both tolerance forms and apply them to
nan(""). Explain the difference from the rule that every NaN comparison is false. - Build the three-magnitude table from §5. State the range of magnitudes over which an absolute tolerance of
1e-9is meaningful. - Sum an array of 1000 values forwards and backwards. How large is the difference, and would your tolerance catch it if it were a bug instead?
- Check whether
__STDC_IEC_559__is defined in your own build. If it is not, what in §6 are you relying on that nobody promised?
11. Summary
real→double,shortreal→float— 8/53/15 against 4/24/6, measured.- IEEE is not a C guarantee:
__STDC_IEC_559__is not defined here, so this is implementation-defined behaviour that happens to be IEEE. - The
realcrossing is exact — five values bit for bit, including5e-324. - Three real losses: the literal (
0.1=0x3FB999999999999A) · each side's arithmetic (addition is not associative, measured) ·shortreal's narrowing (1.49e-09on0.1). - Integers through a
realstop being exact at 2⁵³ —INT64_MAXconverts to one more than itself. - Absolute tolerance is wrong at both ends:
1e-10vanishes at1e9;1e-9passes a doubling at1e-9. - The finding:
d > tolsilently passes a NaN;!(d <= tol)catches it. Identical on every other input. - Check
isnanfirst, use the negated form second, and carry integers as integers.
Every type so far has been a value — bits that are copied across, whatever they mean. The next one is not copied at all. A string crosses as a pointer, and the question stops being what the bits mean and becomes who owns the storage and how long it lasts.
Standards & specifications
- Governing standard
- IEEE Std 1800 (SystemVerilog)(opens IEEE in a new tab)
Defines SystemVerilog language semantics — syntax, data types, scheduling and the behaviour a conforming simulator must produce. It does not define tool-specific synthesis support or vendor methodology.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
