Skip to content

Commit b50a2e6

Browse files
committed
Float16 tweaks: balancing safety and performance
1 parent 070fd60 commit b50a2e6

File tree

1 file changed

+61
-34
lines changed

1 file changed

+61
-34
lines changed

stdlib/public/core/FloatingPointToString.swift

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@
7272
/// * Exponential form always has 1 digit before the decimal point
7373
/// * When present, a '.' is never the first or last character
7474
/// * There is a consecutive range of integer values that can be
75-
/// represented in double (-2^54...2^54). Never use exponential
76-
/// form for integral numbers in this range.
75+
/// represented in any particular type (-2^54...2^54 for double).
76+
/// Never use exponential form for integral numbers in this range.
7777
/// * Generally follow existing practice: Don't use use exponential
7878
/// form for fractional values bigger than 10^-4; always write at
7979
/// least 2 digits for an exponent.
@@ -97,7 +97,33 @@
9797
///
9898
// ----------------------------------------------------------------------------
9999

100+
// Float16 is not currently supported on Intel macOS.
101+
// (This will change once there's a fully-stable Float16
102+
// ABI on that platform.)
103+
#if !((os(macOS) || targetEnvironment(macCatalyst)) && arch(x86_64))
100104
// Implement the legacy ABI on top of the new one
105+
@_silgen_name("swift_float16ToString2")
106+
internal func _float16ToStringImpl2(
107+
_ textBuffer: UnsafeMutablePointer<UTF8.CodeUnit>,
108+
_ bufferLength: UInt,
109+
_ value: Float16,
110+
_ debug: Bool) -> UInt64 {
111+
// Code below works with raw memory.
112+
var buffer = unsafe MutableSpan<UTF8.CodeUnit>(_unchecked: textBuffer,
113+
count: Int(bufferLength))
114+
let textRange = Float16ToASCII(value: value, buffer: &buffer)
115+
let textLength = textRange.upperBound - textRange.lowerBound
116+
117+
// Move the text to the start of the buffer
118+
if textRange.lowerBound != 0 {
119+
unsafe _memmove(dest: textBuffer,
120+
src: textBuffer + textRange.lowerBound,
121+
size: UInt(truncatingIfNeeded: textLength))
122+
}
123+
return UInt64(truncatingIfNeeded: textLength)
124+
}
125+
#endif
126+
101127
@_silgen_name("swift_float32ToString2")
102128
internal func _float32ToStringImpl2(
103129
_ textBuffer: UnsafeMutablePointer<UTF8.CodeUnit>,
@@ -140,7 +166,7 @@ internal func _float64ToStringImpl2(
140166
return UInt64(truncatingIfNeeded: textLength)
141167
}
142168

143-
#if !arch(x86_64)
169+
#if !((os(macOS) || targetEnvironment(macCatalyst)) && arch(x86_64))
144170
internal func Float16ToASCII(
145171
value f: Float16,
146172
buffer utf8Buffer: inout MutableSpan<UTF8.CodeUnit>) -> Range<Int>
@@ -200,6 +226,12 @@ fileprivate func _Float16ToASCII(
200226
var firstDigit = 1
201227
var nextDigit = firstDigit
202228

229+
// Emit the text form differently depending on what range it's in.
230+
// We use `storeBytes(of:toUncheckedByteOffset:as:)` for most of
231+
// the output, but are careful to use the checked/safe form
232+
// `storeBytes(of:toByteOffset:as:)` for the last byte so that we
233+
// reliably crash if we overflow the provided buffer.
234+
203235
// Step 3: If it's < 10^-5, format as exponential form
204236
if binaryExponent < -13 || (binaryExponent == -13 && significand < 0x1a38) {
205237
var decimalExponent = -5
@@ -262,9 +294,10 @@ fileprivate func _Float16ToASCII(
262294
toUncheckedByteOffset: nextDigit,
263295
as: UInt8.self)
264296
nextDigit &+= 1
265-
unsafe buffer.storeBytes(of: UInt8(truncatingIfNeeded: -decimalExponent % 10 &+ 0x30),
266-
toUncheckedByteOffset: nextDigit,
267-
as: UInt8.self)
297+
// Last write on this branch, so use a safe checked store
298+
buffer.storeBytes(of: UInt8(truncatingIfNeeded: -decimalExponent % 10 &+ 0x30),
299+
toByteOffset: nextDigit,
300+
as: UInt8.self)
268301
nextDigit &+= 1
269302

270303
} else {
@@ -312,9 +345,10 @@ fileprivate func _Float16ToASCII(
312345

313346
if fractionPart == 0 {
314347
// Step 6: No fraction, so ".0" and we're done
315-
unsafe buffer.storeBytes(of: 0x30,
316-
toUncheckedByteOffset: nextDigit,
317-
as: UInt8.self)
348+
// Last write on this branch, so use a checked store
349+
buffer.storeBytes(of: 0x30,
350+
toByteOffset: nextDigit,
351+
as: UInt8.self)
318352
nextDigit &+= 1
319353
} else {
320354
// Step 7: Emit the fractional part by repeatedly
@@ -328,15 +362,14 @@ fileprivate func _Float16ToASCII(
328362
while true {
329363
u = (u & mask) &* 10
330364
l = (l & mask) &* 10
331-
// This actually overflows, but we only need the
332-
// low-order bits, so it doesn't matter.
333-
t = (t & mask) &* 10
334365
uDigit = UInt8(truncatingIfNeeded: u >> 28)
335366
lDigit = UInt8(truncatingIfNeeded: l >> 28)
336367
if uDigit != lDigit {
368+
t = (t & mask) &* 10
337369
break
338370
}
339-
371+
// This overflows, but we don't care at this point.
372+
t &*= 10
340373
unsafe buffer.storeBytes(of: 0x30 &+ uDigit,
341374
toUncheckedByteOffset: nextDigit,
342375
as: UInt8.self)
@@ -345,26 +378,28 @@ fileprivate func _Float16ToASCII(
345378
t &+= 1 << 27
346379
if (t & mask) == 0 { // Exactly 1/2
347380
t = (t >> 28) & ~1 // Round last digit even
348-
// Without this next check, 0.015625 == 2^-6 prints
349-
// as "0.01562" which does not round-trip correctly.
350-
// With this, we get "0.01563" which does.
351-
// It affects no other value.
352-
if t <= lDigit && l > 0 {
381+
// Rounding `t` even can end up moving `t` below
382+
// `l`. Detect and correct for this possibility.
383+
// Exhaustive testing shows that the only input value
384+
// affected by this is 0.015625 == 2^-6, which
385+
// incorrectly prints as "0.01562" without this fix.
386+
if t < lDigit || (t == lDigit && l > 0) {
353387
t += 1
354388
}
355389
} else {
356390
t >>= 28
357391
}
358-
unsafe buffer.storeBytes(of: UInt8(truncatingIfNeeded: 0x30 + t),
359-
toUncheckedByteOffset: nextDigit,
360-
as: UInt8.self)
392+
// Last write on this branch, so use a checked store
393+
buffer.storeBytes(of: UInt8(truncatingIfNeeded: 0x30 + t),
394+
toByteOffset: nextDigit,
395+
as: UInt8.self)
361396
nextDigit &+= 1
362397
}
363398
}
364399
if f.sign == .minus {
365-
unsafe buffer.storeBytes(of: 0x2d,
366-
toUncheckedByteOffset: firstDigit &- 1,
367-
as: UInt8.self) // "-"
400+
buffer.storeBytes(of: 0x2d,
401+
toByteOffset: firstDigit &- 1,
402+
as: UInt8.self) // "-"
368403
firstDigit &-= 1
369404
}
370405
return firstDigit..<nextDigit
@@ -982,15 +1017,6 @@ fileprivate func _Float64ToASCII(
9821017
unsafe buffer.storeBytes(of: t,
9831018
toUncheckedByteOffset: nextDigit - 1,
9841019
as: UInt8.self)
985-
// Note: "exactly" 1/2 is a subtle point above; this
986-
// determination relies on various roundings canceling
987-
// out, and proving correctness requires proper
988-
// testing. Testing so far has validated the
989-
// correctness of this code. However, even if that
990-
// were not true, this only affects whether we choose
991-
// the theoretically-ideal even final digit when an
992-
// odd final digit would otherwise satisfy all
993-
// requirements.
9941020
} else {
9951021
let adjust = (skew + oneHalf) >> (64 - adjustIntegerBits)
9961022
var t = unsafe buffer.unsafeLoad(fromUncheckedByteOffset: nextDigit - 1,
@@ -1175,6 +1201,7 @@ fileprivate let asciiDigitTable: InlineArray<100, UInt16> = [
11751201
0x3539, 0x3639, 0x3739, 0x3839, 0x3939
11761202
]
11771203

1204+
// The constants below assume we're on a little-endian processor
11781205
fileprivate func infinity(buffer: inout MutableRawSpan, sign: FloatingPointSign) -> Range<Int> {
11791206
if sign == .minus {
11801207
buffer.storeBytes(of: 0x666e692d, toByteOffset: 0, as: UInt32.self) // "-inf"
@@ -1233,7 +1260,7 @@ fileprivate func nan_details(buffer: inout MutableRawSpan,
12331260
// value is a NaN of some sort
12341261
var i = 0
12351262
if sign == .minus {
1236-
buffer.storeBytes(of: 0x2d, toByteOffset: 0, as: UInt8.self)
1263+
buffer.storeBytes(of: 0x2d, toByteOffset: 0, as: UInt8.self) // "-"
12371264
i = 1
12381265
}
12391266
if quiet {

0 commit comments

Comments
 (0)