Skip to content
VLSI Mentor

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 → C double
  • SystemVerilog shortreal → C float

Measured on this machine:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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.

A property-by-property comparison of the two floating-point crossings, all values measured. SystemVerilog real maps to C double and shortreal maps to C float. Storage is 8 bytes against 4; significand bits are 53 against 24; decimal digits that round-trip are 15 against 6. Sending 0.1 across and reading it back is exact bit for bit through a double and off by 1.49e-09 through a float. So the real crossing costs nothing because 8 bytes go to 8 bytes, and the shortreal crossing costs precision every time because 53 bits are squeezed into 24.Propertymeasured on this machinerealmaps to C doubleshortrealmaps to C floatStoragesizeof the C type8 bytes64 bits4 bytes32 bitsSignificand bitsincluding the implicit bit53DBL_MANT_DIG24FLT_MANT_DIGDecimal digitsthat survive a round trip15DBL_DIG6FLT_DIGSending 0.1 acrossand reading it backexact, bit for bit5 values round-trippedoff by 1.49e-09and 1/3 by 9.93e-09What the crossing costsbefore any arithmeticnothing8 bytes to 8 bytesprecision, always53 bits into 2412
Figure 1 — the two floating-point crossings, measured. Everything above the last row is a property of the C types, and everything the two columns differ on comes from one number: 53 bits of significand against 24. The row that matters most is the fourth — a double sent across and read back is bit-for-bit identical, confirmed over five values including the smallest subnormal and 1e300, so the crossing itself costs nothing at all. The shortreal crossing is different in kind rather than degree: narrowing 53 bits into 24 loses precision on every value that needed more than 24, which is most of them, and the loss happens whether or not anything else goes wrong.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  __STDC_IEC_559__ defined: NO

So 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  double round trip through 8 raw bytes, 5 values: exact

Not 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  double 0.1 = 0x3FB999999999999A  = 0.10000000000000000555

0.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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  ten additions of 0.1 = 0.99999999999999989
  == 1.0 : FALSE
  |x-1.0| = 1.11e-16

And the order matters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  (0.1+0.2)+0.3 = 0.60000000000000009
  0.1+(0.2+0.3) = 0.59999999999999998
  equal: FALSE

Floating-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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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-09

Unlike 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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: no

And on ordinary values they agree:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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: YES

Every 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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/double unless the data is genuinely 32-bit. The crossing is free; the narrowing is not.
  • Carry integers as integers. A longint through a real loses everything past 2⁵³.
  • Never == on a computed value, and never a bare absolute tolerance across magnitudes.
  • Check isnan first, 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 real loses 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 real crossing is exact. Precision is lost in the literal before anything crosses, in each side's arithmetic independently, and in shortreal'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

  1. real → double, shortreal → float — no width to choose, no signedness question.
  2. Measured: double 8 bytes / 53 significand bits / 15 decimal digits; float 4 / 24 / 6.
  3. 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.
  4. The real crossing is exact — five values round-tripped bit for bit, including 5e-324 and 1e300.
  5. Loss one, the literal: 0.1 is stored as 0x3FB999999999999A = 0.10000000000000000555, before anything crosses.
  6. Loss two, the arithmetic: ten additions of 0.1 give 0.99999999999999989, and addition is not associative — (0.1+0.2)+0.3 ≠ 0.1+(0.2+0.3), measured.
  7. Loss three, the narrowing: shortreal costs 1.49e-09 on 0.1 by construction.
  8. A double holds integers exactly only to 2⁵³ — 2⁵³+1 == 2⁵³ is true, and INT64_MAX converts to a value one larger than itself. Carry integers as integers.
  9. Absolute tolerance fails at both ends: at 1e9 a perturbation of 1e-10 vanishes entirely; at 1e-9 a tolerance of 1e-9 passes a 100% error.
  10. The finding: !(d <= tol) reports a NaN mismatch; d > tol does not — measured, identical on every other input. Check isnan first, then use the negated form.

10. Exercises

  1. Print the bit pattern of 0.1 as a double and confirm 0x3FB999999999999A. Then print 0.1 + 0.2 to 20 decimal places.
  2. Round-trip five doubles through their raw bytes and confirm exactness. Include a subnormal.
  3. Narrow 0.1 to float and back; confirm the error is 1.49e-09. Repeat for 1.0/3.0.
  4. Show 2⁵³ + 1 == 2⁵³. Then convert INT64_MAX to double and print it as an integer — which way did it round?
  5. Implement both tolerance forms and apply them to nan(""). Explain the difference from the rule that every NaN comparison is false.
  6. Build the three-magnitude table from §5. State the range of magnitudes over which an absolute tolerance of 1e-9 is meaningful.
  7. 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?
  8. 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 real crossing is exact — five values bit for bit, including 5e-324.
  • Three real losses: the literal (0.1 = 0x3FB999999999999A) · each side's arithmetic (addition is not associative, measured) · shortreal's narrowing (1.49e-09 on 0.1).
  • Integers through a real stop being exact at 2⁵³ — INT64_MAX converts to one more than itself.
  • Absolute tolerance is wrong at both ends: 1e-10 vanishes at 1e9; 1e-9 passes a doubling at 1e-9.
  • The finding: d > tol silently passes a NaN; !(d <= tol) catches it. Identical on every other input.
  • Check isnan first, 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.