mirror of
https://github.com/HDFGroup/hdf5.git
synced 2026-09-25 04:09:44 +03:00
Narrow the canonical float form from %.17e to %.16e
%.17e emits 18 significant digits: one before the decimal point plus 17 after. DBL_DECIMAL_DIG is 17, so that is one more than round-tripping a double requires, and the extra digit is not free. C11 7.22.1.3p11 recommends correct rounding only for decimal forms of at most DECIMAL_DIG significant digits; past that the recommendation weakens to a bounded-value rule. DECIMAL_DIG is sized for long double, so it is 21 where that is x87 80-bit but 17 where long double == double -- MSVC among others. An 18-digit canonical form therefore falls outside p11's recommended range on those targets, for no benefit. %.16e emits exactly 17 significant digits: still DBL_DECIMAL_DIG, still an exact round-trip for every double, and within DECIMAL_DIG everywhere, since DECIMAL_DIG cannot be below 17 wherever double is binary64. Verified over all 2098 powers of two plus 3M random bit patterns: zero round-trip failures. Note the contrast with 7.22.1.3p9, which *requires* correct rounding for the hexadecimal form regardless of digit count. That asymmetry is what canonicalization trades away, and it is why the correctly-rounded conversion requirement exists; the narrower form just avoids making it worse. Stored values change accordingly: 2^-36 now canonicalizes to 1.4551915228366852e-11 rather than 1.45519152283668518e-11. Nothing is released, so there is no compatibility cost.
This commit is contained in:
+1
-1
@@ -1906,7 +1906,7 @@ H5Pappend_filter(hid_t plist_id, H5Z_filter_t filter, unsigned int flags, const
|
||||
/* Persist the caller's parameter string so it can be recovered
|
||||
* losslessly (pipeline v3) without loading the plugin. The string is
|
||||
* canonicalised first -- outer braces stripped, hex-float literals
|
||||
* rewritten to %.17e decimal -- so the persisted bytes are a valid
|
||||
* rewritten to %.16e decimal -- so the persisted bytes are a valid
|
||||
* TOML v1.0.0 document and can be parsed by readers that are not the
|
||||
* HDF5 library. Both normalisations preserve the value exactly.
|
||||
* An empty input stores nothing. */
|
||||
|
||||
+16
-6
@@ -66,9 +66,19 @@ H5Z__copy_chars2(char *out, size_t cap, size_t *pos, const char **p)
|
||||
/*
|
||||
* H5Z__rewrite_hexfloats - return a copy of `src` with every C99 hex-float
|
||||
* literal (e.g. "0x1.8p+1", "-0x1p-1") replaced by an equivalent decimal
|
||||
* string. Uses %.17e, which always carries a decimal point and an exponent
|
||||
* (so tomlc17 types it TOML_FP64, not TOML_INTEGER) and which guarantees
|
||||
* IEEE 754 double round-trip fidelity (C99 DBL_DECIMAL_DIG == 17). %.17g
|
||||
* string. Uses %.16e, which always carries a decimal point and an exponent
|
||||
* (so tomlc17 types it TOML_FP64, not TOML_INTEGER) and which emits exactly
|
||||
* DBL_DECIMAL_DIG == 17 significant digits -- one before the point plus 16
|
||||
* after -- the minimum that round-trips every IEEE 754 double.
|
||||
*
|
||||
* The width is deliberate. C11 7.22.1.3p11 recommends correct rounding only
|
||||
* for decimal forms of at most DECIMAL_DIG significant digits; past that the
|
||||
* recommendation weakens to a bounded-value rule. DECIMAL_DIG is sized for
|
||||
* long double, so it is 21 where that is x87 80-bit but 17 where long double
|
||||
* == double (MSVC among others). %.17e emits 18 digits, exceeding
|
||||
* DECIMAL_DIG on those targets for no benefit, since 17 already round-trip
|
||||
* exactly. Contrast 7.22.1.3p9, which *requires* correct rounding for the
|
||||
* hexadecimal form -- the asymmetry this rewrite trades away. %.17g
|
||||
* must not be substituted: it drops the decimal point for whole values
|
||||
* ("8.0" -> "8"), which a TOML parser reads as an integer.
|
||||
*
|
||||
@@ -89,7 +99,7 @@ H5Z__rewrite_hexfloats(const char *src)
|
||||
char *out;
|
||||
size_t pos = 0;
|
||||
|
||||
/* Worst case: every 3-char token "0x1" expands to ~24 chars "%.17e" -> 8x.
|
||||
/* Worst case: every 3-char token "0x1" expands to ~23 chars "%.16e" -> 8x.
|
||||
* Guard against size_t overflow in the multiplication; callers normally
|
||||
* cap input at H5Z_CONFIG_STRING_MAX, but enforce the bound here too so
|
||||
* this static helper is safe for any future caller. */
|
||||
@@ -179,7 +189,7 @@ H5Z__rewrite_hexfloats(const char *src)
|
||||
* 17 significant digits guarantee IEEE 754
|
||||
* double round-trip fidelity (C99 DBL_DECIMAL_DIG). */
|
||||
char dec[32];
|
||||
int n = snprintf(dec, sizeof(dec), "%.17e", val);
|
||||
int n = snprintf(dec, sizeof(dec), "%.16e", val);
|
||||
/* LC_NUMERIC may replace '.' with the locale decimal
|
||||
* separator (e.g. ',' in de_DE). TOML requires '.'.
|
||||
* Use localeconv() to find the actual separator rather
|
||||
@@ -282,7 +292,7 @@ H5Z__toml_wrap(const char *params)
|
||||
* removed: both "{level = 6}" and "level = 6" store as
|
||||
* "level = 6". This mirrors the acceptance rule in
|
||||
* H5Z__toml_wrap().
|
||||
* 2. C99 hex-float literals are rewritten to %.17e decimal,
|
||||
* 2. C99 hex-float literals are rewritten to %.16e decimal,
|
||||
* which is bit-exact for IEEE 754 doubles.
|
||||
*
|
||||
* The point of both is that the stored bytes are a valid TOML
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ H5_DLL htri_t H5Z_filter_avail(H5Z_filter_t id);
|
||||
H5_DLL herr_t H5Z_delete(struct H5O_pline_t *pline, H5Z_filter_t filter);
|
||||
H5_DLL herr_t H5Z_get_filter_info(H5Z_filter_t filter, unsigned int *filter_config_flags);
|
||||
/* Normalise a parameter string into the form persisted in pipeline v3:
|
||||
* outer braces stripped and hex-float literals rewritten to %.17e decimal,
|
||||
* outer braces stripped and hex-float literals rewritten to %.16e decimal,
|
||||
* so the stored bytes are valid TOML v1.0.0. Caller frees with H5MM_xfree(). */
|
||||
H5_DLL char *H5Z_canonicalize_params(const char *params);
|
||||
|
||||
|
||||
+12
-12
@@ -2459,7 +2459,7 @@ error:
|
||||
*
|
||||
* The stored string is normalised so the bytes on disk are a valid TOML
|
||||
* v1.0.0 document: optional outer braces are stripped, and C99 hex-float
|
||||
* literals are rewritten to %.17e decimal. Neither the braced form nor a
|
||||
* literals are rewritten to %.16e decimal. Neither the braced form nor a
|
||||
* hex-float literal is accepted by a stock TOML parser, and the persisted
|
||||
* string is meant to be readable by tools that are not the HDF5 library
|
||||
* (pure-reimplementation readers such as jHDF and pyfive parse the object
|
||||
@@ -2501,11 +2501,11 @@ canon_get_config(unsigned H5_ATTR_UNUSED flags, size_t cd_nelmts, const unsigned
|
||||
|
||||
if (cd_nelmts >= 2)
|
||||
memcpy(&rate, cd_values, sizeof(rate));
|
||||
needed = (size_t)snprintf(NULL, 0, "rate = %.17e", rate) + 1;
|
||||
needed = (size_t)snprintf(NULL, 0, "rate = %.16e", rate) + 1;
|
||||
if (buf_size)
|
||||
*buf_size = needed;
|
||||
if (buf)
|
||||
snprintf(buf, needed, "rate = %.17e", rate);
|
||||
snprintf(buf, needed, "rate = %.16e", rate);
|
||||
return SUCCEED;
|
||||
}
|
||||
|
||||
@@ -2634,17 +2634,17 @@ test_config_canonicalization(hid_t fapl)
|
||||
TEST_ERROR;
|
||||
PASSED();
|
||||
|
||||
/* --- canon-03: hex-float rewritten to %.17e decimal --- */
|
||||
/* --- canon-03: hex-float rewritten to %.16e decimal --- */
|
||||
TESTING("canonicalization: hex-float rewritten to decimal");
|
||||
if (canon_check("rate = 0x1.8p+1", "rate = 3.00000000000000000e+00") < 0)
|
||||
if (canon_check("rate = 0x1.8p+1", "rate = 3.0000000000000000e+00") < 0)
|
||||
TEST_ERROR;
|
||||
if (canon_check("rate = 0x1.cp+1", "rate = 3.50000000000000000e+00") < 0)
|
||||
if (canon_check("rate = 0x1.cp+1", "rate = 3.5000000000000000e+00") < 0)
|
||||
TEST_ERROR;
|
||||
PASSED();
|
||||
|
||||
/* --- canon-04: both normalisations at once --- */
|
||||
TESTING("canonicalization: braces and hex-float together");
|
||||
if (canon_check("{ rate = 0x1.8p+1 }", "rate = 3.00000000000000000e+00") < 0)
|
||||
if (canon_check("{ rate = 0x1.8p+1 }", "rate = 3.0000000000000000e+00") < 0)
|
||||
TEST_ERROR;
|
||||
PASSED();
|
||||
|
||||
@@ -2716,7 +2716,7 @@ test_config_canonicalization(hid_t fapl)
|
||||
if (H5Pget_filter_params_by_idx(dcpl_out, 0, pbuf, sizeof(pbuf), &plen) < 0)
|
||||
TEST_ERROR;
|
||||
/* Canonical: no outer brace, no hex-float -- parseable as plain TOML */
|
||||
if (strcmp(pbuf, "rate = 3.00000000000000000e+00") != 0)
|
||||
if (strcmp(pbuf, "rate = 3.0000000000000000e+00") != 0)
|
||||
TEST_ERROR;
|
||||
if (pbuf[0] == '{' || strstr(pbuf, "0x") != NULL)
|
||||
TEST_ERROR;
|
||||
@@ -2761,7 +2761,7 @@ test_config_canonicalization(hid_t fapl)
|
||||
if (H5Pclose(dcpl) < 0)
|
||||
TEST_ERROR;
|
||||
dcpl = H5I_INVALID_HID;
|
||||
if (strcmp(s1, "rate = 1.45519152283668518e-11") != 0) {
|
||||
if (strcmp(s1, "rate = 1.4551915228366852e-11") != 0) {
|
||||
fprintf(stderr, "\n stored \"%s\"\n", s1);
|
||||
TEST_ERROR;
|
||||
}
|
||||
@@ -3031,7 +3031,7 @@ test_modify_filter_by_idx(hid_t fapl)
|
||||
TEST_ERROR;
|
||||
if (H5Pget_filter_params_by_idx(dcpl, 0, pbuf, sizeof(pbuf), &plen) < 0)
|
||||
TEST_ERROR;
|
||||
if (strcmp(pbuf, "rate = 3.00000000000000000e+00") != 0)
|
||||
if (strcmp(pbuf, "rate = 3.0000000000000000e+00") != 0)
|
||||
TEST_ERROR;
|
||||
if (H5Pclose(dcpl) < 0)
|
||||
TEST_ERROR;
|
||||
@@ -3055,8 +3055,8 @@ test_modify_filter_by_idx(hid_t fapl)
|
||||
}
|
||||
if (H5Pget_filter_params_by_idx(dcpl, 0, pbuf, sizeof(pbuf), &plen) < 0)
|
||||
TEST_ERROR;
|
||||
/* Stored string gone -> get_config reconstruction, which uses %.17e */
|
||||
if (strcmp(pbuf, "rate = 4.50000000000000000e+00") != 0) {
|
||||
/* Stored string gone -> get_config reconstruction, which uses %.16e */
|
||||
if (strcmp(pbuf, "rate = 4.5000000000000000e+00") != 0) {
|
||||
fprintf(stderr, "\n got \"%s\"\n", pbuf);
|
||||
TEST_ERROR;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user