Skip to content

Commit 97a934c

Browse files
tbkkastephentyrone
authored andcommitted
SR-106: New floating-point description implementation (#15474)
* SR-106: New floating-point `description` implementation This replaces the current implementation of `description` and `debugDescription` for the standard floating-point types with a new formatting routine based on a variation of Florian Loitsch' Grisu2 algorithm with changes suggested by Andrysco, Jhala, and Lerner's 2016 paper describing Errol3. Unlike the earlier code based on `sprintf` with a fixed number of digits, this version always chooses the optimal number of digits. As such, we can now use the exact same output for both `description` and `debugDescription` (except of course that `debugDescription` provides full detail for NaNs). The implementation has been extensively commented; people familiar with Grisu-style algorithms should find the code easy to understand. This implementation is: * Fast. It uses only fixed-width integer arithmetic and has constant memory and time requirements. * Simple. It is only a little more complex than Loitsch' original implementation of Grisu2. The digit decomposition logic for double is less than 300 lines of standard C (half of which is common arithmetic support routines). * Always Accurate. Converting the decimal form back to binary (using an accurate algorithm such as Clinger's) will always yield exactly the original binary value. For the IEEE 754 formats, the round-trip will produce exactly the same bit pattern in memory. This is an essential requirement for JSON serialization, debugging, and logging. * Always Short. This always selects an accurate result with the minimum number of decimal digits. (So that `1.0 / 10.0` will always print `0.1`.) * Always Close. Among all accurate, short results, this always chooses the result that is closest to the exact floating-point value. (In case of an exact tie, it rounds the last digit even.) This resolves SR-106 and related issues that have complained about the floating-point `description` properties being inexact. * Remove duplicate infinity handling * Use defined(__SIZEOF_INT128__) to detect uint128_t support * Separate `extracting` the integer part from `clearing` the integer part The previous code was unnecessarily obfuscated by the attempt to combine these two operations. * Use `UINT32_MAX` to mask off 32 bits of a larger integer * Correct the expected NaN results for 32-bit i386 * Make the C++ exceptions here consistent Adding a C source file somehow exposed an issue in an unrelated C++ file. Thanks to Joe Groff for the fix. * Rename SwiftDtoa to ".cpp" Having a C file in stdlib/public/runtime causes strange build failures on Linux in unrelated C++ files. As a workaround, rename SwiftDtoa.c to .cpp to see if that avoids the problems. * Revert "Make the C++ exceptions here consistent" This reverts commit 6cd5c20.
1 parent d93e0df commit 97a934c

File tree

8 files changed

+3449
-301
lines changed

8 files changed

+3449
-301
lines changed

include/swift/Runtime/SwiftDtoa.h

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//===--- SwiftDtoa.h ---------------------------------------------*- c -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2018 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===---------------------------------------------------------------------===//
12+
13+
#ifndef SWIFT_DTOA_H
14+
#define SWIFT_DTOA_H
15+
16+
#include <stdbool.h>
17+
#include <stdint.h>
18+
#include <stdlib.h>
19+
20+
// This implementation strongly assumes that `float` is
21+
// IEEE 754 single-precision binary32 format and that
22+
// `double` is IEEE 754 double-precision binary64 format.
23+
24+
// Essentially all modern platforms use IEEE 754 floating point
25+
// types now, so enable these by default:
26+
#define SWIFT_DTOA_FLOAT_SUPPORT 1
27+
#define SWIFT_DTOA_DOUBLE_SUPPORT 1
28+
29+
// This implementation assumes `long double` is Intel 80-bit extended format.
30+
#if defined(_WIN32)
31+
// Windows has `long double` == `double` on all platforms, so disable this.
32+
#undef SWIFT_DTOA_FLOAT80_SUPPORT
33+
#elif defined(__APPLE__) || defined(__linux__)
34+
// macOS and Linux support Float80 on X86 hardware but not on ARM
35+
#if defined(__x86_64__) || defined(__i386)
36+
#define SWIFT_DTOA_FLOAT80_SUPPORT 1
37+
#endif
38+
#endif
39+
40+
#ifdef __cplusplus
41+
extern "C" {
42+
#endif
43+
44+
#if SWIFT_DTOA_DOUBLE_SUPPORT
45+
// Compute the optimal decimal digits and exponent for a double.
46+
//
47+
// Input:
48+
// * `d` is the number to be decomposed
49+
// * `digits` is an array of `digits_length`
50+
// * `decimalExponent` is a pointer to an `int`
51+
//
52+
// Ouput:
53+
// * `digits` will receive the decimal digits
54+
// * `decimalExponent` will receive the decimal exponent
55+
// * function returns the number of digits generated
56+
// * the sign of the input number is ignored
57+
//
58+
// Guarantees:
59+
//
60+
// * Accurate. If you parse the result back to a double via an accurate
61+
// algorithm (such as Clinger's algorithm), the resulting double will
62+
// be exactly equal to the original value. On most systems, this
63+
// implies that using `strtod` to parse the output of
64+
// `swift_format_double` will yield exactly the original value.
65+
//
66+
// * Short. No other accurate result will have fewer digits.
67+
//
68+
// * Close. If there are multiple possible decimal forms that are
69+
// both accurate and short, the form computed here will be
70+
// closest to the original binary value.
71+
//
72+
// Notes:
73+
//
74+
// If the input value is infinity or NaN, or `digits_length < 17`, the
75+
// function returns zero and generates no ouput.
76+
//
77+
// If the input value is zero, it will return `decimalExponent = 0` and
78+
// a single digit of value zero.
79+
//
80+
int swift_decompose_double(double d,
81+
int8_t *digits, size_t digits_length, int *decimalExponent);
82+
83+
// Format a double as an ASCII string.
84+
//
85+
// For infinity, it outputs "inf" or "-inf".
86+
//
87+
// For NaN, it outputs a Swift-style detailed dump, including
88+
// sign, signaling/quiet, and payload (if any). Typical output:
89+
// "nan", "-nan", "-snan(0x1234)".
90+
//
91+
// For zero, it outputs "0.0" or "-0.0" depending on the sign.
92+
//
93+
// For other values, it uses `swift_decompose_double` to compute the
94+
// digits, then uses either `swift_format_decimal` or
95+
// `swift_format_exponential` to produce an ASCII string depending on
96+
// the magnitude of the value.
97+
//
98+
// In all cases, it returns the number of ASCII characters actually
99+
// written, or zero if the buffer was too small.
100+
size_t swift_format_double(double, char *dest, size_t length);
101+
#endif
102+
103+
#if SWIFT_DTOA_FLOAT_SUPPORT
104+
// See swift_decompose_double. `digits_length` must be at least 9.
105+
int swift_decompose_float(float f,
106+
int8_t *digits, size_t digits_length, int *decimalExponent);
107+
// See swift_format_double.
108+
size_t swift_format_float(float, char *dest, size_t length);
109+
#endif
110+
111+
#if SWIFT_DTOA_FLOAT80_SUPPORT
112+
// See swift_decompose_double. `digits_length` must be at least 21.
113+
int swift_decompose_float80(long double f,
114+
int8_t *digits, size_t digits_length, int *decimalExponent);
115+
// See swift_format_double.
116+
size_t swift_format_float80(long double, char *dest, size_t length);
117+
#endif
118+
119+
// Generate an ASCII string from the raw exponent and digit information
120+
// as generated by `swift_decompose_double`. Returns the number of
121+
// bytes actually used. If `dest` was not big enough, these functions
122+
// return zero. The generated string is always terminated with a zero
123+
// byte unless `length` was zero.
124+
125+
// "Exponential" form uses common exponential format, e.g., "-1.234e+56"
126+
// The exponent always has a sign and at least two digits. The
127+
// generated string is never longer than `digits_count + 9` bytes,
128+
// including the trailing zero byte.
129+
size_t swift_format_exponential(char *dest, size_t length,
130+
bool negative, const int8_t *digits, int digits_count, int decimalExponent);
131+
132+
// "Decimal" form writes the value without using exponents. This
133+
// includes cases such as "0.000001234", "123.456", and "123456000.0".
134+
// Note that the result always has a decimal point with at least one
135+
// digit before and one digit after. The generated string is never
136+
// longer than `digits_count + abs(exponent) + 4` bytes, including the
137+
// trailing zero byte.
138+
size_t swift_format_decimal(char *dest, size_t length,
139+
bool negative, const int8_t *digits, int digits_count, int decimalExponent);
140+
141+
#ifdef __cplusplus
142+
}
143+
#endif
144+
#endif

stdlib/public/core/FloatingPointTypes.swift.gyb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,29 @@ extension ${Self} : CustomStringConvertible {
8383
/// A textual representation of the value.
8484
@inlinable // FIXME(sil-serialize-all)
8585
public var description: String {
86-
return _float${bits}ToString(self, debug: false)
86+
if isFinite {
87+
return _float${bits}ToString(self, debug: false)
88+
} else if isNaN {
89+
return "nan"
90+
} else if sign == .minus {
91+
return "-inf"
92+
} else {
93+
return "inf"
94+
}
8795
}
8896
}
8997

9098
extension ${Self} : CustomDebugStringConvertible {
9199
/// A textual representation of the value, suitable for debugging.
92100
@inlinable // FIXME(sil-serialize-all)
93101
public var debugDescription: String {
94-
return _float${bits}ToString(self, debug: true)
102+
if isFinite || isNaN {
103+
return _float${bits}ToString(self, debug: true)
104+
} else if sign == .minus {
105+
return "-inf"
106+
} else {
107+
return "inf"
108+
}
95109
}
96110
}
97111

stdlib/public/core/Runtime.swift.gyb

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -407,32 +407,7 @@ internal func _float${bits}ToStringImpl(
407407
internal func _float${bits}ToString(
408408
_ value: Float${bits}, debug: Bool
409409
) -> String {
410-
411-
if !value.isFinite {
412-
let significand = value.significandBitPattern
413-
if significand == 0 {
414-
// Infinity
415-
return value.sign == .minus ? "-inf" : "inf"
416-
}
417-
else {
418-
// NaN
419-
if !debug {
420-
return "nan"
421-
}
422-
let isSignaling = (significand & Float${bits}._quietNaNMask) == 0
423-
let payload = significand & ((Float${bits}._quietNaNMask >> 1) - 1)
424-
// FIXME(performance): Inefficient String manipulation. We could move
425-
// this to C function.
426-
return
427-
(value.sign == .minus ? "-" : "")
428-
+ (isSignaling ? "snan" : "nan")
429-
+ (payload == 0 ? "" : ("(0x" + String(payload, radix: 16) + ")"))
430-
}
431-
}
432-
433410
_sanityCheck(MemoryLayout<_Buffer32>.size == 32)
434-
_sanityCheck(MemoryLayout<_Buffer72>.size == 72)
435-
436411
var buffer = _Buffer32()
437412
return buffer.withBytes { (bufferPtr) in
438413
let actualLength = _float${bits}ToStringImpl(bufferPtr, 32, value, debug)

stdlib/public/runtime/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ set(swift_runtime_sources
6565
ProtocolConformance.cpp
6666
RefCount.cpp
6767
RuntimeInvocationsTracking.cpp
68+
SwiftDtoa.cpp
6869
"${SWIFT_SOURCE_DIR}/lib/Demangling/OldDemangler.cpp"
6970
"${SWIFT_SOURCE_DIR}/lib/Demangling/Demangler.cpp"
7071
"${SWIFT_SOURCE_DIR}/lib/Demangling/NodePrinter.cpp"

0 commit comments

Comments
 (0)