Skip to content

Commit e943878

Browse files
committed
Prefill the work buffer with '0' characters
For example, this avoids the need to do any explicit byte-by-byte writes when expanding "123" out to "123000000.0". This also required reworking the "back out extra digits" process for Float64 to ensure the unused digits get written as '0' characters instead of null bytes.
1 parent 1a3a400 commit e943878

File tree

2 files changed

+59
-47
lines changed

2 files changed

+59
-47
lines changed

stdlib/public/core/FloatingPointToString.swift

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,24 @@ public func _float16ToStringImpl(
155155
return UInt64(truncatingIfNeeded: textLength)
156156
}
157157

158+
// Convert a Float16 to an optimal ASCII representation.
159+
// See notes above for comments on the output format here.
160+
// Inputs:
161+
// * `value`: Float16 input
162+
// * `buffer`: Buffer to place the result
163+
// Returns: Range of bytes within `buffer` that contain the result
164+
//
165+
// Buffer must be at least 32 bytes long and must be pre-filled
166+
// with "0" characters, e.g., via
167+
// `InlineArray<32,UTF8.CodeUnit>(repeating:0x30)`
168+
158169
@available(SwiftStdlib 6.2, *)
159170
internal func _Float16ToASCII(
160171
value f: Float16,
161172
buffer utf8Buffer: inout MutableSpan<UTF8.CodeUnit>
162173
) -> Range<Int> {
163174
// We need a MutableRawSpan in order to use wide store/load operations
175+
// TODO: Tune this value down to the actual minimum for Float16
164176
precondition(utf8Buffer.count >= 32)
165177
var buffer = utf8Buffer.mutableBytes
166178

@@ -338,11 +350,7 @@ internal func _Float16ToASCII(
338350

339351
if fractionPart == 0 {
340352
// Step 6: No fraction, so ".0" and we're done
341-
// Last write on this branch, so use a checked store
342-
buffer.storeBytes(
343-
of: 0x30,
344-
toByteOffset: nextDigit,
345-
as: UInt8.self)
353+
// "0" write is free since buffer is pre-initialized
346354
nextDigit &+= 1
347355
} else {
348356
// Step 7: Emit the fractional part by repeatedly
@@ -439,6 +447,17 @@ public func _float32ToStringImpl(
439447
}
440448
}
441449

450+
// Convert a Float32 to an optimal ASCII representation.
451+
// See notes above for comments on the output format here.
452+
// See _Float64ToASCII for comments on the algorithm.
453+
// Inputs:
454+
// * `value`: Float32 input
455+
// * `buffer`: Buffer to place the result
456+
// Returns: Range of bytes within `buffer` that contain the result
457+
//
458+
// Buffer must be at least 32 bytes long and must be pre-filled
459+
// with "0" characters, e.g., via
460+
// `InlineArray<32,UTF8.CodeUnit>(repeating:0x30)`
442461
@available(SwiftStdlib 6.2, *)
443462
internal func _Float32ToASCII(
444463
value f: Float32,
@@ -449,6 +468,8 @@ internal func _Float32ToASCII(
449468
// more detailed comments and explanation.
450469

451470
// We need a MutableRawSpan in order to use wide store/load operations
471+
// TODO: Tune this limit down to the actual minimum we need here
472+
// TODO: `assert` that the buffer is filled with 0x30 bytes (in debug builds)
452473
precondition(utf8Buffer.count >= 32)
453474
var buffer = utf8Buffer.mutableBytes
454475

@@ -561,12 +582,6 @@ internal func _Float32ToASCII(
561582
var delta = u &- l
562583
let fractionMask: UInt64 = (1 << fractionBits) - 1
563584

564-
// Write 8 leading zeros to the beginning of the buffer:
565-
unsafe buffer.storeBytes(
566-
of: 0x3030303030303030,
567-
toUncheckedByteOffset: 0,
568-
as: UInt64.self)
569-
570585
// Overwrite the first digit at index 7:
571586
let firstDigit = 7
572587
let digit = (t >> fractionBits) &+ 0x30
@@ -684,6 +699,18 @@ public func _float64ToStringImpl(
684699
}
685700
}
686701

702+
// Convert a Float64 to an optimal ASCII representation.
703+
// See notes above for comments on the output format here.
704+
// The algorithm is extensively commented inline; the comments
705+
// at the top of this source file give additional context.
706+
// Inputs:
707+
// * `value`: Float64 input
708+
// * `buffer`: Buffer to place the result
709+
// Returns: Range of bytes within `buffer` that contain the result
710+
//
711+
// Buffer must be at least 32 bytes long and must be pre-filled
712+
// with "0" characters, e.g., via
713+
// `InlineArray<32,UTF8.CodeUnit>(repeating:0x30)`
687714
@available(SwiftStdlib 6.2, *)
688715
internal func _Float64ToASCII(
689716
value d: Float64,
@@ -937,10 +964,6 @@ internal func _Float64ToASCII(
937964

938965
var nextDigit = 5
939966
var firstDigit = nextDigit
940-
unsafe buffer.storeBytes(
941-
of: 0x3030303030303030 as UInt64,
942-
toUncheckedByteOffset: 0,
943-
as: UInt64.self)
944967

945968
// Our initial scaling gave us the first 7 digits already:
946969
let d12345678 = UInt32(truncatingIfNeeded: t._high >> 32)
@@ -1015,13 +1038,14 @@ internal func _Float64ToASCII(
10151038
t0 &= ~1
10161039
}
10171040
// t0 has t0digits digits. Write them out
1018-
let text = _intToEightDigits(t0) >> ((8 - t0digits) * 8)
1041+
let text = _intToEightDigits(t0)
10191042
buffer.storeBytes(
10201043
of: text,
10211044
toByteOffset: nextDigit,
10221045
as: UInt64.self)
1023-
nextDigit &+= t0digits
1024-
firstDigit &+= 1
1046+
nextDigit &+= 8
1047+
// Skip the leading zeros
1048+
firstDigit &+= 9 - t0digits
10251049
} else {
10261050
// Our initial scaling did not produce too many digits. The
10271051
// `d12345678` value holds the first 7 digits (plus a leading
@@ -1182,6 +1206,17 @@ internal func _float80ToStringImpl(
11821206
}
11831207
}
11841208

1209+
// Convert a Float80 to an optimal ASCII representation.
1210+
// See notes above for comments on the output format here.
1211+
// See _Float64ToASCII for comments on the algorithm.
1212+
// Inputs:
1213+
// * `value`: Float80 input
1214+
// * `buffer`: Buffer to place the result
1215+
// Returns: Range of bytes within `buffer` that contain the result
1216+
//
1217+
// Buffer must be at least 32 bytes long and must be pre-filled
1218+
// with "0" characters, e.g., via
1219+
// `InlineArray<32,UTF8.CodeUnit>(repeating:0x30)`
11851220
@available(SwiftStdlib 6.2, *)
11861221
internal func _Float80ToASCII(
11871222
value f: Float80,
@@ -1408,13 +1443,7 @@ fileprivate func _backend_256bit(
14081443

14091444
// Step 7: Generate digits
14101445

1411-
// Include 8 "0" characters at the beginning of the buffer
1412-
// for finishFormatting to use
1413-
buffer.storeBytes(
1414-
of: 0x3030303030303030,
1415-
toByteOffset: 0,
1416-
as: UInt64.self)
1417-
// Start writing digits just after that
1446+
// Leave 8 bytes at the beginning for finishFormatting to use
14181447
let firstDigit = 8
14191448
var nextDigit = firstDigit
14201449
buffer.storeBytes(
@@ -1526,7 +1555,7 @@ fileprivate func _backend_256bit(
15261555
// inserting decimal points, minus signs, exponents, etc, as
15271556
// necessary. To minimize the work here, this assumes that there are
15281557
// at least 5 unused bytes at the beginning of `buffer` before
1529-
// `firstDigit` and that those bytes are filled with `"0"` (0x30)
1558+
// `firstDigit` and that all unused bytes are filled with `"0"` (0x30)
15301559
// characters.
15311560

15321561
@available(SwiftStdlib 6.2, *)
@@ -1646,25 +1675,8 @@ fileprivate func _finishFormatting(
16461675
// "12345678900.0"
16471676
// Fill trailing zeros, put ".0" at the end
16481677
// so the result is obviously floating-point.
1649-
let zeroEnd = firstDigit &+ base10Exponent &+ 3
1650-
// TODO: Find out how to use C memset() here:
1651-
// Blast 8 "0" digits into the buffer
1652-
buffer.storeBytes(
1653-
of: 0x3030303030303030 as UInt64,
1654-
toByteOffset: nextDigit,
1655-
as: UInt64.self)
1656-
// Add more "0" digits if needed...
1657-
// (Note: Can't use a standard range loop because nextDigit+8
1658-
// can legitimately be larger than zeroEnd here.)
1659-
var i = nextDigit + 8
1660-
while i < zeroEnd {
1661-
unsafe buffer.storeBytes(
1662-
of: 0x30,
1663-
toUncheckedByteOffset: i,
1664-
as: UInt8.self)
1665-
i &+= 1
1666-
}
1667-
nextDigit = zeroEnd
1678+
// Remember buffer was initialized with "0"
1679+
nextDigit = firstDigit &+ base10Exponent &+ 3
16681680
buffer.storeBytes(
16691681
of: 0x2e,
16701682
toByteOffset: nextDigit &- 2,

stdlib/public/core/FloatingPointTypes.swift.gyb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ extension ${Self}: CustomDebugStringConvertible {
119119
/// that NaN values are printed in an extended format.
120120
public var debugDescription: String {
121121
if #available(SwiftStdlib 6.2, *) {
122-
var buffer = InlineArray<64, UTF8.CodeUnit>(repeating: 0)
122+
var buffer = InlineArray<64, UTF8.CodeUnit>(repeating: 0x30)
123123
var span = buffer.mutableSpan
124124
let textRange = _Float${bits}ToASCII(value: self, buffer: &span)
125125
let textStart = unsafe span._start().assumingMemoryBound(to: UTF8.CodeUnit.self) + textRange.lowerBound
@@ -138,7 +138,7 @@ ${Availability(bits)}
138138
extension ${Self}: TextOutputStreamable {
139139
public func write<Target>(to target: inout Target) where Target: TextOutputStream {
140140
if #available(SwiftStdlib 6.2, *) {
141-
var buffer = InlineArray<64, UTF8.CodeUnit>(repeating: 0)
141+
var buffer = InlineArray<64, UTF8.CodeUnit>(repeating: 0x30)
142142
var span = buffer.mutableSpan
143143
let textRange = _Float${bits}ToASCII(value: self, buffer: &span)
144144
let textStart = unsafe span._start().assumingMemoryBound(to: UTF8.CodeUnit.self) + textRange.lowerBound

0 commit comments

Comments
 (0)