From 6ae47a3fe3877277c8ac2cd4cab5137f4e97b737 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 15 Jul 2025 13:35:35 -0700 Subject: [PATCH 01/14] WIP vectorization for UTF16->UTF8 --- stdlib/public/core/StringCreate.swift | 62 ++++++- stdlib/public/core/UTF16.swift | 250 ++++++++++++++++++++++++++ 2 files changed, 311 insertions(+), 1 deletion(-) diff --git a/stdlib/public/core/StringCreate.swift b/stdlib/public/core/StringCreate.swift index a4ccbbfdcd033..59c6aa2b19491 100644 --- a/stdlib/public/core/StringCreate.swift +++ b/stdlib/public/core/StringCreate.swift @@ -247,6 +247,44 @@ extension String { initializingFrom: input, isASCII: isASCII) return storage.asString } + + @usableFromInline + internal static func _fromUTF16( + _ input: UnsafeBufferPointer, + repairing: Bool = true + ) -> (String, repairsMade: Bool)? { + guard let (utf8Len, isASCII) = utf8Length( + of: input, + repairing: repairing + ) else { + return nil + } + if utf8Len <= _SmallString.capacity { + let smol = try unsafe _SmallString(initializingUTF8With: { + return transcodeUTF16ToUTF8( + UTF16CodeUnits: input, + into: buffer, + repairing: repairing + ) + }) + return String(_StringGuts(smol)) + } + let result = unsafe __StringStorage.create( + uninitializedCodeUnitCapacity: utf8Len, + initializingUncheckedUTF8With: { buffer -> Int in + return transcodeUTF16ToUTF8( + UTF16CodeUnits: input, + into: buffer, + repairing: repairing + ) + } + ) + result._updateCountAndFlags( + newCount: result.count, + newIsASCII: isASCII + ) + return result.asString + } @usableFromInline internal static func _uncheckedFromUTF16( @@ -311,7 +349,29 @@ extension String { repair: Bool ) -> (String, repairsMade: Bool)? where Input.Element == Encoding.CodeUnit { - guard _fastPath(encoding == Unicode.ASCII.self) else { + switch encoding { + case Unicode.ASCII.self: + break + case Unicode.UTF16.self: +#if !$Embedded + if let contigBytes = input as? _HasContiguousBytes, + contigBytes._providesContiguousBytesNoCopy { + contigBytes.withUnsafeBytes { rawBufPtr in + let buffer = unsafe UnsafeBufferPointer( + start: rawBufPtr.baseAddress?.assumingMemoryBound(to: UInt16.self), + count: rawBufPtr.count) + return unsafe _fromUTF16(buffer, repair: repair) + } + } +#endif + if let str = input.withContiguousStorageIfAvailable({ + (buffer: UnsafeBufferPointer) -> String? in + return unsafe _fromUTF16(buffer, repair: repair) + }) { + return str + } + fallthrough + default: return _slowFromCodeUnits(input, encoding: encoding, repair: repair) } diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 0c2f159f66890..7d3fe0ec7bc1f 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -434,3 +434,253 @@ extension Unicode.UTF16.ForwardParser: Unicode.Parser, _UTFParser { return r } } + +private enum ScalarFallbackResult: UInt8 { + case invalid + case singleByte + case multiByte +} + +#if arch(arm64_32) +typealias Word = UInt64 +#else +typealias Word = UInt +#endif +let mask = Word(truncatingIfNeeded: 0x80808080_80808080 as UInt64) + +#if (arch(arm64) || arch(arm64_32))// && SWIFT_STDLIB_ENABLE_VECTOR_TYPES +typealias Block = (SIMD8, SIMD8) +@_transparent func umaxv(_ vec: SIMD8) -> UInt16 { + UInt16(Builtin.int_vector_reduce_umax_Vec8xInt16(vec._storage._value)) +} +#else +typealias Block = (Word, Word, Word, Word) +#endif + +@_transparent +func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { + let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) +#if (arch(arm64) || arch(arm64_32))// && SWIFT_STDLIB_ENABLE_VECTOR_TYPES + return umaxv(block.0 | block.1) < 0x80 ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil +#else + let mask = Word(truncatingIfNeeded: 0x80808080_80808080 as UInt64) + return (block.0 | block.1 | block.2 | block.3) & mask == 0 +#endif +} + +private func processNonASCIIScalarFallback( + _ cu: UInt16, + input: inout UnsafePointer, + inputEnd: UnsafePointer, + output: inout UnsafeMutablePointer, + outputEnd: UnsafePointer, + repairing: Bool +) -> ScalarFallbackResult { + var scalar = Unicode.UTF16.encodedReplacementCharacter + if UTF16.isLeadSurrogate(cu) { + if input + 1 < inputEnd { + let next = (input + 1).pointee + if UTF16.isTrailSurrogate(next) { + scalar = Unicode.UTF16.EncodedScalar( + _storage: UnsafeRawPointer(input).loadUnaligned(as: UInt32.self), + _bitCount: 32 + ) + } + } + } else if !UTF16.isTrailSurrogate(cu) { + scalar = Unicode.UTF16.EncodedScalar(_storage: UInt32(cu), _bitCount: 16) + } + if !repairing { + if Unicode.UTF16.decode(scalar).value == 0xFFFD { + return .invalid + } + } + input += scalar.count + let utf8Scalar = Unicode.UTF8.transcode(scalar, from: Unicode.UTF16.self).unsafelyUnwrapped + for byte in utf8Scalar { + if output >= outputEnd { + return .invalid + } + output.initialize(to: byte) + output += 1 + } + return .multiByte +} + +private func processScalarFallback( + input: inout UnsafePointer, + inputEnd: UnsafePointer, + output: inout UnsafeMutablePointer, + outputEnd: UnsafePointer, + repairing: Bool +) -> ScalarFallbackResult { + let cu = input.pointee + if Unicode.UTF16.isASCII(cu) { + if output < outputEnd { + output.initialize(to: UInt8(truncatingIfNeeded: cu)) + input += 1 + output += 1 + } else { + Builtin.unreachable() + } + } else { + // Scalar fallback for this code unit + return processNonASCIIScalarFallback( + cu, + input: &input, + inputEnd: inputEnd, + output: &output, + outputEnd: outputEnd, + repairing: repairing + ) + } + return .singleByte +} + +func processNonASCIIChunk( + input: inout UnsafePointer, + inputEnd: UnsafePointer, + output: inout UnsafeMutablePointer, + outputEnd: UnsafePointer, + repairing: Bool +) -> Bool { + for _ in 0 ..< 16 { + switch processScalarFallback( + input: &input, + inputEnd: inputEnd, + output: &output, + outputEnd: outputEnd, + repairing: repairing + ) { + case .invalid: + return false + case .multiByte: + return true //found the non-ASCII, try starting a new SIMD batch + case .singleByte: + continue + } + } + Builtin.unreachable() +} + +internal func transcodeUTF16ToUTF8( + UTF16CodeUnits: UnsafeBufferPointer, + into outputBuffer: UnsafeMutableBufferPointer, + repairing: Bool = true +) -> Int { + let inCount = UTF16CodeUnits.count + let outCount = outputBuffer.count + guard inCount > 0, outCount > 0 else { return 0 } + var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped + let inputEnd = input + inCount + let inputEnd256 = input + inCount & ~(MemoryLayout>.stride &- 1) + var output = outputBuffer.baseAddress.unsafelyUnwrapped + let outputStart = output + let outputEnd = output + outCount + let outputEnd256 = output + outCount & ~(MemoryLayout>.stride &- 1) + + while input < inputEnd256 && output < outputEnd256 { + if let asciiBlock = allASCIIBlock(at: input) { + // All ASCII: transcode directly + for i in 0..<16 { + (output + i).initialize(to: asciiBlock[i]) + } + input += 16 + output += 16 + } else { + if !processNonASCIIChunk( + input: &input, + inputEnd: inputEnd, + output: &output, + outputEnd: outputEnd, + repairing: repairing + ) { + return output - outputStart + } + } + } + // Finish any remaining code units using fallback scalar loop + while input < inputEnd && output < outputEnd { + let scalarFallBackResult = processScalarFallback( + input: &input, + inputEnd: inputEnd, + output: &output, + outputEnd: outputEnd, + repairing: repairing + ) + if scalarFallBackResult ~= .invalid { + return output - outputStart + } + } + return output - outputStart +} + +internal func utf8Length( + of UTF16CodeUnits: UnsafeBufferPointer, + repairing: Bool = true +) -> (Int, isASCII: Bool) ? { + let inCount = UTF16CodeUnits.count + guard inCount > 0 else { return 0 } + var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped + let inputEnd = input + inCount + let inputEnd256 = input + inCount & ~(MemoryLayout>.stride &- 1) + var count = 0 + var isASCII = true + while input < inputEnd256 { + if let asciiBlock = allASCIIBlock(at: input) { + input += 16 + count += 16 + } else { + isASCII = false + var tmp: ( + UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32 + ) = (0, 0, 0, 0, 0, 0, 0, 0) + let chunkCount = withUnsafeMutableBytes(of: &tmp) { outputBuf -> Int? in + var output = outputBuf.baseAddress.unsafelyUnwrapped.assumingMemoryBound(to: UInt8.self) + let outputStart = output + let outputEnd = output + outputBuf.count + if !processNonASCIIChunk( + input: &input, + inputEnd: inputEnd, + output: &output, + outputEnd: outputEnd, + repairing: repairing + ) { + return nil + } + return output - outputStart + } + if let chunkCount { + count += chunkCount + } else { + return nil + } + } + } + // Finish any remaining input that didn't fit in a SIMD chunk + while input < inputEnd { + var tmp: UInt32 = 0 + let trailingCount = withUnsafeMutableBytes(of: &tmp) { outputBuf -> Int? in + var output = outputBuf.baseAddress.unsafelyUnwrapped.assumingMemoryBound(to: UInt8.self) + let outputStart = output + let outputEnd = output + outputBuf.count + let scalarFallBackResult = processScalarFallback( + input: &input, + inputEnd: inputEnd, + output: &output, + outputEnd: outputEnd, + repairing: repairing + ) + if scalarFallBackResult ~= .invalid { + return nil + } + return output - outputStart + } + if let trailingCount { + count += trailingCount + } else { + return nil + } + } + return (count, isASCII: isASCII) +} From 246939c58fb568ae773476d6c23a6d7c24503cd9 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 15 Jul 2025 15:00:32 -0700 Subject: [PATCH 02/14] Lots of fixes --- stdlib/public/core/StringCreate.swift | 52 ++++++++------- stdlib/public/core/UTF16.swift | 92 ++++++++++++++++----------- 2 files changed, 83 insertions(+), 61 deletions(-) diff --git a/stdlib/public/core/StringCreate.swift b/stdlib/public/core/StringCreate.swift index 59c6aa2b19491..2b34aab2e1c76 100644 --- a/stdlib/public/core/StringCreate.swift +++ b/stdlib/public/core/StringCreate.swift @@ -259,31 +259,36 @@ extension String { ) else { return nil } + var repairsMade = false if utf8Len <= _SmallString.capacity { let smol = try unsafe _SmallString(initializingUTF8With: { - return transcodeUTF16ToUTF8( + let (count, tmpRepairsMade) = transcodeUTF16ToUTF8( UTF16CodeUnits: input, - into: buffer, + into: $0, repairing: repairing ) + repairsMade = tmpRepairsMade + return count }) - return String(_StringGuts(smol)) + return (String(_StringGuts(smol)), repairsMade: repairsMade) } let result = unsafe __StringStorage.create( uninitializedCodeUnitCapacity: utf8Len, initializingUncheckedUTF8With: { buffer -> Int in - return transcodeUTF16ToUTF8( + let (count, tmpRepairsMade) = transcodeUTF16ToUTF8( UTF16CodeUnits: input, into: buffer, repairing: repairing ) + repairsMade = tmpRepairsMade + return count } ) result._updateCountAndFlags( newCount: result.count, newIsASCII: isASCII ) - return result.asString + return (result.asString, repairsMade: repairsMade) } @usableFromInline @@ -349,29 +354,28 @@ extension String { repair: Bool ) -> (String, repairsMade: Bool)? where Input.Element == Encoding.CodeUnit { - switch encoding { - case Unicode.ASCII.self: - break - case Unicode.UTF16.self: + if encoding != Unicode.ASCII.self { + if encoding == Unicode.UTF16.self { #if !$Embedded - if let contigBytes = input as? _HasContiguousBytes, - contigBytes._providesContiguousBytesNoCopy { - contigBytes.withUnsafeBytes { rawBufPtr in - let buffer = unsafe UnsafeBufferPointer( - start: rawBufPtr.baseAddress?.assumingMemoryBound(to: UInt16.self), - count: rawBufPtr.count) - return unsafe _fromUTF16(buffer, repair: repair) + if let contigBytes = input as? _HasContiguousBytes, + contigBytes._providesContiguousBytesNoCopy { + return contigBytes.withUnsafeBytes { rawBufPtr -> (String, repairsMade: Bool)? in + let buffer = unsafe UnsafeBufferPointer( + start: rawBufPtr.baseAddress?.assumingMemoryBound(to: UInt16.self), + count: rawBufPtr.count) + return unsafe _fromUTF16(buffer, repairing: repair) + } } - } #endif - if let str = input.withContiguousStorageIfAvailable({ - (buffer: UnsafeBufferPointer) -> String? in - return unsafe _fromUTF16(buffer, repair: repair) - }) { - return str + if let str = input.withContiguousStorageIfAvailable({ + (buffer: UnsafeBufferPointer) -> (String, repairsMade: Bool)? in + return unsafe buffer.withMemoryRebound(to: UInt16.self) { + return unsafe _fromUTF16($0, repairing: repair) + } + }) { + return str + } } - fallthrough - default: return _slowFromCodeUnits(input, encoding: encoding, repair: repair) } diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 7d3fe0ec7bc1f..be7a39ee9e0da 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -446,9 +446,9 @@ typealias Word = UInt64 #else typealias Word = UInt #endif -let mask = Word(truncatingIfNeeded: 0x80808080_80808080 as UInt64) +let mask = Word(truncatingIfNeeded: 0xFF80FF80_FF80FF80 as UInt64) -#if (arch(arm64) || arch(arm64_32))// && SWIFT_STDLIB_ENABLE_VECTOR_TYPES +#if (arch(arm64) || arch(arm64_32)) typealias Block = (SIMD8, SIMD8) @_transparent func umaxv(_ vec: SIMD8) -> UInt16 { UInt16(Builtin.int_vector_reduce_umax_Vec8xInt16(vec._storage._value)) @@ -457,16 +457,27 @@ typealias Block = (SIMD8, SIMD8) typealias Block = (Word, Word, Word, Word) #endif +#if _pointerBitWidth(_32) && !arch(arm64_32) +@_transparent +func allASCIIBlock(at pointer: UnsafePointer) -> SIMD8? { + let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) +#if (arch(arm64) || arch(arm64_32)) + return umaxv(block.0 | block.1) < 0x80 ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil +#else + return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil +#endif +} +#else @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) -#if (arch(arm64) || arch(arm64_32))// && SWIFT_STDLIB_ENABLE_VECTOR_TYPES +#if (arch(arm64) || arch(arm64_32)) return umaxv(block.0 | block.1) < 0x80 ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil #else - let mask = Word(truncatingIfNeeded: 0x80808080_80808080 as UInt64) - return (block.0 | block.1 | block.2 | block.3) & mask == 0 + return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil #endif } +#endif private func processNonASCIIScalarFallback( _ cu: UInt16, @@ -475,7 +486,7 @@ private func processNonASCIIScalarFallback( output: inout UnsafeMutablePointer, outputEnd: UnsafePointer, repairing: Bool -) -> ScalarFallbackResult { +) -> (ScalarFallbackResult, repairsMade: Bool) { var scalar = Unicode.UTF16.encodedReplacementCharacter if UTF16.isLeadSurrogate(cu) { if input + 1 < inputEnd { @@ -490,21 +501,23 @@ private func processNonASCIIScalarFallback( } else if !UTF16.isTrailSurrogate(cu) { scalar = Unicode.UTF16.EncodedScalar(_storage: UInt32(cu), _bitCount: 16) } - if !repairing { - if Unicode.UTF16.decode(scalar).value == 0xFFFD { - return .invalid + var repairsMade = false + if Unicode.UTF16.decode(scalar).value == 0xFFFD { + if !repairing { + return (.invalid, repairsMade: repairsMade) } + repairsMade = true } input += scalar.count let utf8Scalar = Unicode.UTF8.transcode(scalar, from: Unicode.UTF16.self).unsafelyUnwrapped for byte in utf8Scalar { if output >= outputEnd { - return .invalid + return (.invalid, repairsMade: repairsMade) } output.initialize(to: byte) output += 1 } - return .multiByte + return (.multiByte, repairsMade: repairsMade) } private func processScalarFallback( @@ -513,7 +526,7 @@ private func processScalarFallback( output: inout UnsafeMutablePointer, outputEnd: UnsafePointer, repairing: Bool -) -> ScalarFallbackResult { +) -> (ScalarFallbackResult, repairsMade: Bool) { let cu = input.pointee if Unicode.UTF16.isASCII(cu) { if output < outputEnd { @@ -534,7 +547,7 @@ private func processScalarFallback( repairing: repairing ) } - return .singleByte + return (.singleByte, repairsMade: false) } func processNonASCIIChunk( @@ -543,7 +556,7 @@ func processNonASCIIChunk( output: inout UnsafeMutablePointer, outputEnd: UnsafePointer, repairing: Bool -) -> Bool { +) -> (Bool, repairsMade: Bool) { for _ in 0 ..< 16 { switch processScalarFallback( input: &input, @@ -552,11 +565,11 @@ func processNonASCIIChunk( outputEnd: outputEnd, repairing: repairing ) { - case .invalid: - return false - case .multiByte: - return true //found the non-ASCII, try starting a new SIMD batch - case .singleByte: + case (.invalid, let repairsMade): + return (false, repairsMade: repairsMade) + case (.multiByte, let repairsMade): + return (true, repairsMade: repairsMade) //found the non-ASCII, try starting a new SIMD batch + case (.singleByte, _): continue } } @@ -567,17 +580,18 @@ internal func transcodeUTF16ToUTF8( UTF16CodeUnits: UnsafeBufferPointer, into outputBuffer: UnsafeMutableBufferPointer, repairing: Bool = true -) -> Int { +) -> (Int, repairsMade: Bool) { let inCount = UTF16CodeUnits.count let outCount = outputBuffer.count - guard inCount > 0, outCount > 0 else { return 0 } + guard inCount > 0, outCount > 0 else { return (0, repairsMade: false) } var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped let inputEnd = input + inCount - let inputEnd256 = input + inCount & ~(MemoryLayout>.stride &- 1) + let inputEnd256 = input + (inCount - (inCount % 16)) var output = outputBuffer.baseAddress.unsafelyUnwrapped let outputStart = output let outputEnd = output + outCount - let outputEnd256 = output + outCount & ~(MemoryLayout>.stride &- 1) + let outputEnd256 = output + (outCount - (outCount % 8)) + var repairsMade = false while input < inputEnd256 && output < outputEnd256 { if let asciiBlock = allASCIIBlock(at: input) { @@ -588,46 +602,50 @@ internal func transcodeUTF16ToUTF8( input += 16 output += 16 } else { - if !processNonASCIIChunk( + let (success, tmpRepairsMade) = processNonASCIIChunk( input: &input, inputEnd: inputEnd, output: &output, outputEnd: outputEnd, repairing: repairing - ) { - return output - outputStart + ) + repairsMade = repairsMade && tmpRepairsMade + if !success { + return (output - outputStart, repairsMade: repairsMade) } } } // Finish any remaining code units using fallback scalar loop while input < inputEnd && output < outputEnd { - let scalarFallBackResult = processScalarFallback( + switch processScalarFallback( input: &input, inputEnd: inputEnd, output: &output, outputEnd: outputEnd, repairing: repairing - ) - if scalarFallBackResult ~= .invalid { - return output - outputStart + ) { + case (.invalid, let tmpRepairsMade): + return (output - outputStart, repairsMade: repairsMade && tmpRepairsMade) + case (_, let tmpRepairsMade): + repairsMade = repairsMade && tmpRepairsMade } } - return output - outputStart + return (output - outputStart, repairsMade: repairsMade) } internal func utf8Length( of UTF16CodeUnits: UnsafeBufferPointer, repairing: Bool = true -) -> (Int, isASCII: Bool) ? { +) -> (Int, isASCII: Bool)? { let inCount = UTF16CodeUnits.count - guard inCount > 0 else { return 0 } + guard inCount > 0 else { return (0, isASCII: true) } var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped let inputEnd = input + inCount - let inputEnd256 = input + inCount & ~(MemoryLayout>.stride &- 1) + let inputEnd256 = input + (inCount - (inCount % 16)) var count = 0 var isASCII = true while input < inputEnd256 { - if let asciiBlock = allASCIIBlock(at: input) { + if let _ = allASCIIBlock(at: input) { input += 16 count += 16 } else { @@ -645,7 +663,7 @@ internal func utf8Length( output: &output, outputEnd: outputEnd, repairing: repairing - ) { + ).0 { return nil } return output - outputStart @@ -664,7 +682,7 @@ internal func utf8Length( var output = outputBuf.baseAddress.unsafelyUnwrapped.assumingMemoryBound(to: UInt8.self) let outputStart = output let outputEnd = output + outputBuf.count - let scalarFallBackResult = processScalarFallback( + let (scalarFallBackResult, _) = processScalarFallback( input: &input, inputEnd: inputEnd, output: &output, From f85efe5aeedba9151723d425eeabc064159cbb01 Mon Sep 17 00:00:00 2001 From: David Smith Date: Wed, 16 Jul 2025 15:09:48 -0700 Subject: [PATCH 03/14] Fun fact: UInt16 is not the same size as UInt8 --- stdlib/public/core/StringCreate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/public/core/StringCreate.swift b/stdlib/public/core/StringCreate.swift index 2b34aab2e1c76..785730239ce86 100644 --- a/stdlib/public/core/StringCreate.swift +++ b/stdlib/public/core/StringCreate.swift @@ -362,7 +362,7 @@ extension String { return contigBytes.withUnsafeBytes { rawBufPtr -> (String, repairsMade: Bool)? in let buffer = unsafe UnsafeBufferPointer( start: rawBufPtr.baseAddress?.assumingMemoryBound(to: UInt16.self), - count: rawBufPtr.count) + count: rawBufPtr.count / 2) return unsafe _fromUTF16(buffer, repairing: repair) } } From 4b84ced2d5d0dc138e5de3dc56ea5028ff204087 Mon Sep 17 00:00:00 2001 From: David Smith Date: Fri, 18 Jul 2025 23:50:18 -0700 Subject: [PATCH 04/14] See if the scalar version autovectorizes on arm64 too --- stdlib/public/core/UTF16.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index be7a39ee9e0da..be8120755040e 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -471,11 +471,11 @@ func allASCIIBlock(at pointer: UnsafePointer) -> SIMD8? { @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) -#if (arch(arm64) || arch(arm64_32)) - return umaxv(block.0 | block.1) < 0x80 ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil -#else +//#if (arch(arm64) || arch(arm64_32)) +// return umaxv(block.0 | block.1) < 0x80 ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil +//#else return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil -#endif +//#endif } #endif @@ -644,6 +644,7 @@ internal func utf8Length( let inputEnd256 = input + (inCount - (inCount % 16)) var count = 0 var isASCII = true + while input < inputEnd256 { if let _ = allASCIIBlock(at: input) { input += 16 From 25ac9705a021ae537181a6933b0b3ecf4ef2efae Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 19 Jul 2025 12:24:26 -0700 Subject: [PATCH 05/14] Build fix for experiment --- stdlib/public/core/UTF16.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index be8120755040e..590805507aba3 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -448,14 +448,14 @@ typealias Word = UInt #endif let mask = Word(truncatingIfNeeded: 0xFF80FF80_FF80FF80 as UInt64) -#if (arch(arm64) || arch(arm64_32)) -typealias Block = (SIMD8, SIMD8) -@_transparent func umaxv(_ vec: SIMD8) -> UInt16 { - UInt16(Builtin.int_vector_reduce_umax_Vec8xInt16(vec._storage._value)) -} -#else +//#if (arch(arm64) || arch(arm64_32)) +//typealias Block = (SIMD8, SIMD8) +//@_transparent func umaxv(_ vec: SIMD8) -> UInt16 { +// UInt16(Builtin.int_vector_reduce_umax_Vec8xInt16(vec._storage._value)) +//} +//#else typealias Block = (Word, Word, Word, Word) -#endif +//#endif #if _pointerBitWidth(_32) && !arch(arm64_32) @_transparent From 931ae62ed244b689c496c59f469ce3ca59a37290 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 19 Jul 2025 13:18:46 -0700 Subject: [PATCH 06/14] Remove arm64-specific code --- stdlib/public/core/UTF16.swift | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 590805507aba3..7aed967ce6c42 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -448,34 +448,19 @@ typealias Word = UInt #endif let mask = Word(truncatingIfNeeded: 0xFF80FF80_FF80FF80 as UInt64) -//#if (arch(arm64) || arch(arm64_32)) -//typealias Block = (SIMD8, SIMD8) -//@_transparent func umaxv(_ vec: SIMD8) -> UInt16 { -// UInt16(Builtin.int_vector_reduce_umax_Vec8xInt16(vec._storage._value)) -//} -//#else typealias Block = (Word, Word, Word, Word) -//#endif #if _pointerBitWidth(_32) && !arch(arm64_32) @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD8? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) -#if (arch(arm64) || arch(arm64_32)) - return umaxv(block.0 | block.1) < 0x80 ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil -#else return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil -#endif } #else @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) -//#if (arch(arm64) || arch(arm64_32)) -// return umaxv(block.0 | block.1) < 0x80 ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil -//#else return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil -//#endif } #endif From 8e9f5e028fdf083dda3a232a26c8730e3df50d55 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sat, 19 Jul 2025 13:34:29 -0700 Subject: [PATCH 07/14] Adjust for 32 bit --- stdlib/public/core/UTF16.swift | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 7aed967ce6c42..5a03f69c9f1d3 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -451,12 +451,14 @@ let mask = Word(truncatingIfNeeded: 0xFF80FF80_FF80FF80 as UInt64) typealias Block = (Word, Word, Word, Word) #if _pointerBitWidth(_32) && !arch(arm64_32) +@_transparent var blockSize:Int { 8 } @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD8? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil } #else +@_transparent var blockSize:Int { 16 } @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) @@ -542,7 +544,7 @@ func processNonASCIIChunk( outputEnd: UnsafePointer, repairing: Bool ) -> (Bool, repairsMade: Bool) { - for _ in 0 ..< 16 { + for _ in 0 ..< blockSize { switch processScalarFallback( input: &input, inputEnd: inputEnd, @@ -571,21 +573,21 @@ internal func transcodeUTF16ToUTF8( guard inCount > 0, outCount > 0 else { return (0, repairsMade: false) } var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped let inputEnd = input + inCount - let inputEnd256 = input + (inCount - (inCount % 16)) + let inputEnd256 = input + (inCount - (inCount % blockSize)) var output = outputBuffer.baseAddress.unsafelyUnwrapped let outputStart = output let outputEnd = output + outCount - let outputEnd256 = output + (outCount - (outCount % 8)) + let outputEnd256 = output + (outCount - (outCount % (blockSize / 2))) var repairsMade = false while input < inputEnd256 && output < outputEnd256 { if let asciiBlock = allASCIIBlock(at: input) { // All ASCII: transcode directly - for i in 0..<16 { + for i in 0 ..< blockSize { (output + i).initialize(to: asciiBlock[i]) } - input += 16 - output += 16 + input += blockSize + output += blockSize } else { let (success, tmpRepairsMade) = processNonASCIIChunk( input: &input, @@ -626,14 +628,14 @@ internal func utf8Length( guard inCount > 0 else { return (0, isASCII: true) } var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped let inputEnd = input + inCount - let inputEnd256 = input + (inCount - (inCount % 16)) + let inputEnd256 = input + (inCount - (inCount % blockSize)) var count = 0 var isASCII = true while input < inputEnd256 { if let _ = allASCIIBlock(at: input) { - input += 16 - count += 16 + input += blockSize + count += blockSize } else { isASCII = false var tmp: ( From f0cee2534e5e7fb268d3601e209d07bccc9a6067 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 20 Jul 2025 02:30:34 -0700 Subject: [PATCH 08/14] Stop doing size math, stop duplicating work in some cases, and delete an unnecessary usableFromInline --- stdlib/public/core/StringCreate.swift | 29 ++++++++++++++------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/stdlib/public/core/StringCreate.swift b/stdlib/public/core/StringCreate.swift index 785730239ce86..8cfefd7a0d3be 100644 --- a/stdlib/public/core/StringCreate.swift +++ b/stdlib/public/core/StringCreate.swift @@ -248,7 +248,6 @@ extension String { return storage.asString } - @usableFromInline internal static func _fromUTF16( _ input: UnsafeBufferPointer, repairing: Bool = true @@ -356,25 +355,27 @@ extension String { where Input.Element == Encoding.CodeUnit { if encoding != Unicode.ASCII.self { if encoding == Unicode.UTF16.self { + if let str = input.withContiguousStorageIfAvailable({ buffer in + unsafe _fromUTF16( + UnsafeRawBufferPointer(buffer).assumingMemoryBound(to: UInt16.self), + repairing: repair + ) + }) { + return str + } #if !$Embedded if let contigBytes = input as? _HasContiguousBytes, contigBytes._providesContiguousBytesNoCopy { - return contigBytes.withUnsafeBytes { rawBufPtr -> (String, repairsMade: Bool)? in - let buffer = unsafe UnsafeBufferPointer( - start: rawBufPtr.baseAddress?.assumingMemoryBound(to: UInt16.self), - count: rawBufPtr.count / 2) - return unsafe _fromUTF16(buffer, repairing: repair) + if let str = contigBytes.withUnsafeBytes({ buffer in + unsafe _fromUTF16( + buffer.assumingMemoryBound(to: UInt16.self), + repairing: repair + ) + }) { + return str } } #endif - if let str = input.withContiguousStorageIfAvailable({ - (buffer: UnsafeBufferPointer) -> (String, repairsMade: Bool)? in - return unsafe buffer.withMemoryRebound(to: UInt16.self) { - return unsafe _fromUTF16($0, repairing: repair) - } - }) { - return str - } } return _slowFromCodeUnits(input, encoding: encoding, repair: repair) } From 4b9be8fedbcca0ba6fcbcc46025a61e9ec5fa929 Mon Sep 17 00:00:00 2001 From: David Smith Date: Sun, 20 Jul 2025 03:23:51 -0700 Subject: [PATCH 09/14] Adopt the new implementation in another place, add unsafe annotations, and special case empty buffers --- stdlib/public/core/StringCreate.swift | 25 ++---- stdlib/public/core/UTF16.swift | 119 +++++++++++++------------- 2 files changed, 68 insertions(+), 76 deletions(-) diff --git a/stdlib/public/core/StringCreate.swift b/stdlib/public/core/StringCreate.swift index 8cfefd7a0d3be..d02099513ca09 100644 --- a/stdlib/public/core/StringCreate.swift +++ b/stdlib/public/core/StringCreate.swift @@ -252,7 +252,8 @@ extension String { _ input: UnsafeBufferPointer, repairing: Bool = true ) -> (String, repairsMade: Bool)? { - guard let (utf8Len, isASCII) = utf8Length( + if input.isEmpty { return ("", repairsMade: false) } + guard let (utf8Len, isASCII) = unsafe utf8Length( of: input, repairing: repairing ) else { @@ -260,8 +261,8 @@ extension String { } var repairsMade = false if utf8Len <= _SmallString.capacity { - let smol = try unsafe _SmallString(initializingUTF8With: { - let (count, tmpRepairsMade) = transcodeUTF16ToUTF8( + let smol = unsafe _SmallString(initializingUTF8With: { + let (count, tmpRepairsMade) = unsafe transcodeUTF16ToUTF8( UTF16CodeUnits: input, into: $0, repairing: repairing @@ -274,7 +275,7 @@ extension String { let result = unsafe __StringStorage.create( uninitializedCodeUnitCapacity: utf8Len, initializingUncheckedUTF8With: { buffer -> Int in - let (count, tmpRepairsMade) = transcodeUTF16ToUTF8( + let (count, tmpRepairsMade) = unsafe transcodeUTF16ToUTF8( UTF16CodeUnits: input, into: buffer, repairing: repairing @@ -294,21 +295,9 @@ extension String { internal static func _uncheckedFromUTF16( _ input: UnsafeBufferPointer ) -> String { - // TODO(String Performance): Attempt to form smol strings - - // TODO(String performance): Skip intermediary array, transcode directly - // into a StringStorage space. - var contents: [UInt8] = [] - contents.reserveCapacity(input.count) - let repaired = unsafe transcode( - input.makeIterator(), - from: UTF16.self, - to: UTF8.self, - stoppingOnError: false, - into: { contents.append($0) }) + let (result, repaired) = unsafe _fromUTF16(input, repairing: true)! _internalInvariant(!repaired, "Error present") - - return unsafe contents.withUnsafeBufferPointer { unsafe String._uncheckedFromUTF8($0) } + return result } @inline(never) // slow path diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 5a03f69c9f1d3..9ecb6359fb2b6 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -455,14 +455,14 @@ typealias Block = (Word, Word, Word, Word) @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD8? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) - return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil + return unsafe ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil } #else @_transparent var blockSize:Int { 16 } @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) - return ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil + return unsafe ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil } #endif @@ -476,10 +476,10 @@ private func processNonASCIIScalarFallback( ) -> (ScalarFallbackResult, repairsMade: Bool) { var scalar = Unicode.UTF16.encodedReplacementCharacter if UTF16.isLeadSurrogate(cu) { - if input + 1 < inputEnd { - let next = (input + 1).pointee + if unsafe input + 1 < inputEnd { + let next = unsafe (input + 1).pointee if UTF16.isTrailSurrogate(next) { - scalar = Unicode.UTF16.EncodedScalar( + scalar = unsafe Unicode.UTF16.EncodedScalar( _storage: UnsafeRawPointer(input).loadUnaligned(as: UInt32.self), _bitCount: 32 ) @@ -495,14 +495,17 @@ private func processNonASCIIScalarFallback( } repairsMade = true } - input += scalar.count - let utf8Scalar = Unicode.UTF8.transcode(scalar, from: Unicode.UTF16.self).unsafelyUnwrapped + unsafe input += scalar.count + let utf8Scalar = unsafe Unicode.UTF8.transcode( + scalar, + from: Unicode.UTF16.self + ).unsafelyUnwrapped for byte in utf8Scalar { - if output >= outputEnd { + if unsafe output >= outputEnd { return (.invalid, repairsMade: repairsMade) } - output.initialize(to: byte) - output += 1 + unsafe output.initialize(to: byte) + unsafe output += 1 } return (.multiByte, repairsMade: repairsMade) } @@ -514,18 +517,18 @@ private func processScalarFallback( outputEnd: UnsafePointer, repairing: Bool ) -> (ScalarFallbackResult, repairsMade: Bool) { - let cu = input.pointee + let cu = unsafe input.pointee if Unicode.UTF16.isASCII(cu) { - if output < outputEnd { - output.initialize(to: UInt8(truncatingIfNeeded: cu)) - input += 1 - output += 1 + if unsafe output < outputEnd { + unsafe output.initialize(to: UInt8(truncatingIfNeeded: cu)) + unsafe input += 1 + unsafe output += 1 } else { Builtin.unreachable() } } else { // Scalar fallback for this code unit - return processNonASCIIScalarFallback( + return unsafe processNonASCIIScalarFallback( cu, input: &input, inputEnd: inputEnd, @@ -545,7 +548,7 @@ func processNonASCIIChunk( repairing: Bool ) -> (Bool, repairsMade: Bool) { for _ in 0 ..< blockSize { - switch processScalarFallback( + switch unsafe processScalarFallback( input: &input, inputEnd: inputEnd, output: &output, @@ -571,25 +574,25 @@ internal func transcodeUTF16ToUTF8( let inCount = UTF16CodeUnits.count let outCount = outputBuffer.count guard inCount > 0, outCount > 0 else { return (0, repairsMade: false) } - var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped - let inputEnd = input + inCount - let inputEnd256 = input + (inCount - (inCount % blockSize)) - var output = outputBuffer.baseAddress.unsafelyUnwrapped - let outputStart = output - let outputEnd = output + outCount - let outputEnd256 = output + (outCount - (outCount % (blockSize / 2))) + var input = unsafe UTF16CodeUnits.baseAddress.unsafelyUnwrapped + let inputEnd = unsafe input + inCount + let inputEnd256 = unsafe input + (inCount - (inCount % blockSize)) + var output = unsafe outputBuffer.baseAddress.unsafelyUnwrapped + let outputStart = unsafe output + let outputEnd = unsafe output + outCount + let outputEnd256 = unsafe output + (outCount - (outCount % (blockSize / 2))) var repairsMade = false - while input < inputEnd256 && output < outputEnd256 { - if let asciiBlock = allASCIIBlock(at: input) { + while unsafe input < inputEnd256 && output < outputEnd256 { + if let asciiBlock = unsafe allASCIIBlock(at: input) { // All ASCII: transcode directly for i in 0 ..< blockSize { - (output + i).initialize(to: asciiBlock[i]) + unsafe (output + i).initialize(to: asciiBlock[i]) } - input += blockSize - output += blockSize + unsafe input += blockSize + unsafe output += blockSize } else { - let (success, tmpRepairsMade) = processNonASCIIChunk( + let (success, tmpRepairsMade) = unsafe processNonASCIIChunk( input: &input, inputEnd: inputEnd, output: &output, @@ -598,13 +601,13 @@ internal func transcodeUTF16ToUTF8( ) repairsMade = repairsMade && tmpRepairsMade if !success { - return (output - outputStart, repairsMade: repairsMade) + return unsafe (output - outputStart, repairsMade: repairsMade) } } } // Finish any remaining code units using fallback scalar loop - while input < inputEnd && output < outputEnd { - switch processScalarFallback( + while unsafe input < inputEnd && output < outputEnd { + switch unsafe processScalarFallback( input: &input, inputEnd: inputEnd, output: &output, @@ -612,12 +615,12 @@ internal func transcodeUTF16ToUTF8( repairing: repairing ) { case (.invalid, let tmpRepairsMade): - return (output - outputStart, repairsMade: repairsMade && tmpRepairsMade) + return unsafe (output - outputStart, repairsMade: repairsMade && tmpRepairsMade) case (_, let tmpRepairsMade): repairsMade = repairsMade && tmpRepairsMade } } - return (output - outputStart, repairsMade: repairsMade) + return unsafe (output - outputStart, repairsMade: repairsMade) } internal func utf8Length( @@ -626,26 +629,25 @@ internal func utf8Length( ) -> (Int, isASCII: Bool)? { let inCount = UTF16CodeUnits.count guard inCount > 0 else { return (0, isASCII: true) } - var input = UTF16CodeUnits.baseAddress.unsafelyUnwrapped - let inputEnd = input + inCount - let inputEnd256 = input + (inCount - (inCount % blockSize)) + var input = unsafe UTF16CodeUnits.baseAddress.unsafelyUnwrapped + let inputEnd = unsafe input + inCount + let inputEnd256 = unsafe input + (inCount - (inCount % blockSize)) var count = 0 var isASCII = true - while input < inputEnd256 { - if let _ = allASCIIBlock(at: input) { - input += blockSize + while unsafe input < inputEnd256 { + if let _ = unsafe allASCIIBlock(at: input) { + unsafe input += blockSize count += blockSize } else { isASCII = false - var tmp: ( - UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32, UInt32 - ) = (0, 0, 0, 0, 0, 0, 0, 0) - let chunkCount = withUnsafeMutableBytes(of: &tmp) { outputBuf -> Int? in - var output = outputBuf.baseAddress.unsafelyUnwrapped.assumingMemoryBound(to: UInt8.self) - let outputStart = output - let outputEnd = output + outputBuf.count - if !processNonASCIIChunk( + let chunkCount = unsafe withUnsafeTemporaryAllocation( + of: UInt8.self, capacity: blockSize * 4 /*max 4 bytes per UTF8 element*/ + ) { outputBuf -> Int? in + var output = unsafe outputBuf.baseAddress.unsafelyUnwrapped + let outputStart = unsafe output + let outputEnd = unsafe output + outputBuf.count + if unsafe !processNonASCIIChunk( input: &input, inputEnd: inputEnd, output: &output, @@ -654,7 +656,7 @@ internal func utf8Length( ).0 { return nil } - return output - outputStart + return unsafe output - outputStart } if let chunkCount { count += chunkCount @@ -664,13 +666,14 @@ internal func utf8Length( } } // Finish any remaining input that didn't fit in a SIMD chunk - while input < inputEnd { - var tmp: UInt32 = 0 - let trailingCount = withUnsafeMutableBytes(of: &tmp) { outputBuf -> Int? in - var output = outputBuf.baseAddress.unsafelyUnwrapped.assumingMemoryBound(to: UInt8.self) - let outputStart = output - let outputEnd = output + outputBuf.count - let (scalarFallBackResult, _) = processScalarFallback( + while unsafe input < inputEnd { + let trailingCount = unsafe withUnsafeTemporaryAllocation( + of: UInt8.self, capacity: 4 /*max 4 bytes per UTF8 element*/ + ) { outputBuf -> Int? in + var output = unsafe outputBuf.baseAddress.unsafelyUnwrapped + let outputStart = unsafe output + let outputEnd = unsafe output + outputBuf.count + let (scalarFallBackResult, _) = unsafe processScalarFallback( input: &input, inputEnd: inputEnd, output: &output, @@ -680,7 +683,7 @@ internal func utf8Length( if scalarFallBackResult ~= .invalid { return nil } - return output - outputStart + return unsafe output - outputStart } if let trailingCount { count += trailingCount From f326f61ca5a58b3d4c8681d211244b0bf3c14c7b Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Jul 2025 12:14:01 -0700 Subject: [PATCH 10/14] Actually detect non-ascii in the fallback path --- stdlib/public/core/UTF16.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 9ecb6359fb2b6..844aab1bffa49 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -680,7 +680,13 @@ internal func utf8Length( outputEnd: outputEnd, repairing: repairing ) - if scalarFallBackResult ~= .invalid { + switch scalarFallBackResult { + case .singleByte: + break + case .multiByte: + isASCII = false + break + case .invalid: return nil } return unsafe output - outputStart From 9263ce6729e9fa43a2a941e62f282e038bd382f9 Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 21 Jul 2025 13:59:09 -0700 Subject: [PATCH 11/14] Remove pointless failed attempt at being clever --- stdlib/public/core/UTF16.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 844aab1bffa49..29e53323eca79 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -576,14 +576,12 @@ internal func transcodeUTF16ToUTF8( guard inCount > 0, outCount > 0 else { return (0, repairsMade: false) } var input = unsafe UTF16CodeUnits.baseAddress.unsafelyUnwrapped let inputEnd = unsafe input + inCount - let inputEnd256 = unsafe input + (inCount - (inCount % blockSize)) var output = unsafe outputBuffer.baseAddress.unsafelyUnwrapped let outputStart = unsafe output let outputEnd = unsafe output + outCount - let outputEnd256 = unsafe output + (outCount - (outCount % (blockSize / 2))) var repairsMade = false - while unsafe input < inputEnd256 && output < outputEnd256 { + while unsafe input < (inputEnd - blockSize) && output < (outputEnd - (blockSize / 2)) { if let asciiBlock = unsafe allASCIIBlock(at: input) { // All ASCII: transcode directly for i in 0 ..< blockSize { @@ -631,11 +629,10 @@ internal func utf8Length( guard inCount > 0 else { return (0, isASCII: true) } var input = unsafe UTF16CodeUnits.baseAddress.unsafelyUnwrapped let inputEnd = unsafe input + inCount - let inputEnd256 = unsafe input + (inCount - (inCount % blockSize)) var count = 0 var isASCII = true - while unsafe input < inputEnd256 { + while unsafe input < (inputEnd - blockSize) { if let _ = unsafe allASCIIBlock(at: input) { unsafe input += blockSize count += blockSize From 317795705789049c59c0049bfb718bfd9d71dab9 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 22 Jul 2025 01:49:16 -0700 Subject: [PATCH 12/14] Do it all by hand, since empirically it's a lot faster for runs of non-ascii --- stdlib/public/core/UTF16.swift | 142 ++++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 36 deletions(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 29e53323eca79..c9a6e05494f11 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -446,7 +446,9 @@ typealias Word = UInt64 #else typealias Word = UInt #endif -let mask = Word(truncatingIfNeeded: 0xFF80FF80_FF80FF80 as UInt64) +@_transparent var mask:Word { + Word(truncatingIfNeeded: 0xFF80FF80_FF80FF80 as UInt64) +} typealias Block = (Word, Word, Word, Word) @@ -455,61 +457,129 @@ typealias Block = (Word, Word, Word, Word) @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD8? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) - return unsafe ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil + return unsafe ((block.0 | block.1 | block.2 | block.3) & mask == 0) + ? unsafeBitCast(block, to: SIMD16.self).evenHalf : nil } #else @_transparent var blockSize:Int { 16 } @_transparent func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { let block = unsafe UnsafeRawPointer(pointer).loadUnaligned(as: Block.self) - return unsafe ((block.0 | block.1 | block.2 | block.3) & mask == 0) ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil + return unsafe ((block.0 | block.1 | block.2 | block.3) & mask == 0) + ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil } #endif +@_transparent var utf8TwoByteMax: UInt32 { 0x7FF } +@_transparent var utf16LeadSurrogateMin: UInt32 { 0xD800 } +@_transparent var utf16TrailSurrogateMin: UInt32 { 0xDC00 } +@_transparent var utf16ReplacementCharacter: UInt32 { 0xFFFD } +@_transparent var utf16ScalarMax: UInt32 { 0x10FFFF } +@_transparent var utf16BasicMultilingualPlaneMax: UInt32 { 0xFFFF } +@_transparent var utf16AstralPlaneMin: UInt32 { 0x10000 } + +/* + This is expressible in a more concise way using the other transcoding + primitives in the stdlib, but at least as of July 2025 doing that makes + processing runs of non-ASCII several times slower. + */ +@inline(__always) +private func encodeScalarAsUTF8( + _ scalar: UInt32, + output: inout UnsafeMutablePointer, + outputEnd: UnsafePointer, +) -> ScalarFallbackResult { + _debugPrecondition(scalar > 0x80) + _debugPrecondition(scalar <= utf16ScalarMax) + if scalar <= utf8TwoByteMax { + if output + 2 > outputEnd { return .invalid } + // Scalar fits in 11 bits + // 2 byte UTF8 is 0b110[top 5 bits] 0b10[bottom 6 bits] + output.pointee = 0b1100_0000 | UInt8((scalar >> 6) & 0b01_1111) + (output + 1).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) + output += 2 + } else if scalar <= utf16BasicMultilingualPlaneMax { + // Scalar fits in 16 bits + // 3 byte UTF8 is 0b1110[top 4 bits] 0b10[middle 6 bits] 0b10[bottom 6 bits] + if output + 3 > outputEnd { return .invalid } + output.pointee = 0b1110_0000 | UInt8((scalar >> 12) & 0b1111) + (output + 1).pointee = 0b1000_0000 | UInt8((scalar >> 6) & 0b11_1111) + (output + 2).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) + output += 3 + } else if scalar <= utf16ScalarMax { + // Scalar fits in 21 bits. + // 0b11110[top 3] 0b10[upper middle 6] 0b10[lower middle 6] 0b10[bottom 6] + if output + 4 > outputEnd { return .invalid } + output.pointee = 0b1111_0000 | UInt8((scalar >> 18) & 0b0111) + (output + 1).pointee = 0b1000_0000 | UInt8((scalar >> 12) & 0b11_1111) + (output + 2).pointee = 0b1000_0000 | UInt8((scalar >> 6) & 0b11_1111) + (output + 3).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) + output += 4 + } else { + Builtin.unreachable() + } + return .multiByte +} + +@inline(__always) private func processNonASCIIScalarFallback( _ cu: UInt16, - input: inout UnsafePointer, - inputEnd: UnsafePointer, + input: inout UnsafePointer, + inputEnd: UnsafePointer, output: inout UnsafeMutablePointer, outputEnd: UnsafePointer, repairing: Bool ) -> (ScalarFallbackResult, repairsMade: Bool) { - var scalar = Unicode.UTF16.encodedReplacementCharacter + var scalar: UInt32 = 0 + var invalid = false if UTF16.isLeadSurrogate(cu) { - if unsafe input + 1 < inputEnd { - let next = unsafe (input + 1).pointee - if UTF16.isTrailSurrogate(next) { - scalar = unsafe Unicode.UTF16.EncodedScalar( - _storage: UnsafeRawPointer(input).loadUnaligned(as: UInt32.self), - _bitCount: 32 - ) + if input + 1 >= inputEnd { + //Leading with no room for trailing + invalid = true + input += 1 + } else { + let next = (input + 1).pointee + if !UTF16.isTrailSurrogate(next) { + //Leading followed by non-trailing + invalid = true + input += 1 + } else { + /* + Code points outside the BMP are encoded as: + value -= smallest non-BMP code point + lead = smallest leading surrogate + high 10 bits of value + trail = smallest trailing surrogate + low 10 bits of value + */ + scalar = utf16AstralPlaneMin + + ((UInt32(cu) - utf16LeadSurrogateMin) << 10) + + (UInt32(next) - utf16TrailSurrogateMin) + input += 2 } } - } else if !UTF16.isTrailSurrogate(cu) { - scalar = Unicode.UTF16.EncodedScalar(_storage: UInt32(cu), _bitCount: 16) - } - var repairsMade = false - if Unicode.UTF16.decode(scalar).value == 0xFFFD { - if !repairing { - return (.invalid, repairsMade: repairsMade) - } - repairsMade = true - } - unsafe input += scalar.count - let utf8Scalar = unsafe Unicode.UTF8.transcode( - scalar, - from: Unicode.UTF16.self - ).unsafelyUnwrapped - for byte in utf8Scalar { - if unsafe output >= outputEnd { - return (.invalid, repairsMade: repairsMade) - } - unsafe output.initialize(to: byte) - unsafe output += 1 + } else if UTF16.isTrailSurrogate(cu) { + //Trailing with no leading + invalid = true + input += 1 + } else { + scalar = UInt32(cu) + input += 1 + } + if _slowPath(invalid || scalar > utf16ScalarMax) { + guard repairing else { return (.invalid, repairsMade: false) } + return ( + encodeScalarAsUTF8(utf16ReplacementCharacter, + output: &output, + outputEnd: outputEnd), + repairsMade: true + ) } - return (.multiByte, repairsMade: repairsMade) + return ( + encodeScalarAsUTF8(scalar, output: &output, outputEnd: outputEnd), + repairsMade: false + ) } +@inline(__always) private func processScalarFallback( input: inout UnsafePointer, inputEnd: UnsafePointer, @@ -581,7 +651,7 @@ internal func transcodeUTF16ToUTF8( let outputEnd = unsafe output + outCount var repairsMade = false - while unsafe input < (inputEnd - blockSize) && output < (outputEnd - (blockSize / 2)) { + while unsafe input <= (inputEnd - blockSize) && output <= (outputEnd - (blockSize / 2)) { if let asciiBlock = unsafe allASCIIBlock(at: input) { // All ASCII: transcode directly for i in 0 ..< blockSize { From 9dc0c96851fa7572366db0bd19cebafa1d2f29ed Mon Sep 17 00:00:00 2001 From: David Smith Date: Mon, 28 Jul 2025 14:44:46 -0700 Subject: [PATCH 13/14] Add a (slow) scalar fallback path, and add more unsafe annotations --- stdlib/public/core/UTF16.swift | 64 ++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 26 deletions(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index c9a6e05494f11..15d8f1590142b 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -452,6 +452,7 @@ typealias Word = UInt typealias Block = (Word, Word, Word, Word) +#if SWIFT_STDLIB_ENABLE_VECTOR_TYPES #if _pointerBitWidth(_32) && !arch(arm64_32) @_transparent var blockSize:Int { 8 } @_transparent @@ -469,6 +470,17 @@ func allASCIIBlock(at pointer: UnsafePointer) -> SIMD16? { ? unsafeBitCast(block, to: SIMD32.self).evenHalf : nil } #endif +#else +@_transparent var blockSize:Int { 1 } +@_transparent +func allASCIIBlock(at pointer: UnsafePointer) -> CollectionOfOne? { + let value = unsafe pointer.pointee + if value & 0xFF80 == 0 { + return CollectionOfOne(UInt8(truncatingIfNeeded: value)) + } + return nil +} +#endif @_transparent var utf8TwoByteMax: UInt32 { 0x7FF } @_transparent var utf16LeadSurrogateMin: UInt32 { 0xD800 } @@ -492,29 +504,29 @@ private func encodeScalarAsUTF8( _debugPrecondition(scalar > 0x80) _debugPrecondition(scalar <= utf16ScalarMax) if scalar <= utf8TwoByteMax { - if output + 2 > outputEnd { return .invalid } + if unsafe output + 2 > outputEnd { return .invalid } // Scalar fits in 11 bits // 2 byte UTF8 is 0b110[top 5 bits] 0b10[bottom 6 bits] - output.pointee = 0b1100_0000 | UInt8((scalar >> 6) & 0b01_1111) - (output + 1).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) - output += 2 + unsafe output.pointee = 0b1100_0000 | UInt8((scalar >> 6) & 0b01_1111) + unsafe (output + 1).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) + unsafe output += 2 } else if scalar <= utf16BasicMultilingualPlaneMax { // Scalar fits in 16 bits // 3 byte UTF8 is 0b1110[top 4 bits] 0b10[middle 6 bits] 0b10[bottom 6 bits] - if output + 3 > outputEnd { return .invalid } - output.pointee = 0b1110_0000 | UInt8((scalar >> 12) & 0b1111) - (output + 1).pointee = 0b1000_0000 | UInt8((scalar >> 6) & 0b11_1111) - (output + 2).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) - output += 3 + if unsafe output + 3 > outputEnd { return .invalid } + unsafe output.pointee = 0b1110_0000 | UInt8((scalar >> 12) & 0b1111) + unsafe (output + 1).pointee = 0b1000_0000 | UInt8((scalar >> 6) & 0b11_1111) + unsafe (output + 2).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) + unsafe output += 3 } else if scalar <= utf16ScalarMax { // Scalar fits in 21 bits. // 0b11110[top 3] 0b10[upper middle 6] 0b10[lower middle 6] 0b10[bottom 6] - if output + 4 > outputEnd { return .invalid } - output.pointee = 0b1111_0000 | UInt8((scalar >> 18) & 0b0111) - (output + 1).pointee = 0b1000_0000 | UInt8((scalar >> 12) & 0b11_1111) - (output + 2).pointee = 0b1000_0000 | UInt8((scalar >> 6) & 0b11_1111) - (output + 3).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) - output += 4 + if unsafe output + 4 > outputEnd { return .invalid } + unsafe output.pointee = 0b1111_0000 | UInt8((scalar >> 18) & 0b0111) + unsafe (output + 1).pointee = 0b1000_0000 | UInt8((scalar >> 12) & 0b11_1111) + unsafe (output + 2).pointee = 0b1000_0000 | UInt8((scalar >> 6) & 0b11_1111) + unsafe (output + 3).pointee = 0b1000_0000 | UInt8(scalar & 0b11_1111) + unsafe output += 4 } else { Builtin.unreachable() } @@ -533,16 +545,16 @@ private func processNonASCIIScalarFallback( var scalar: UInt32 = 0 var invalid = false if UTF16.isLeadSurrogate(cu) { - if input + 1 >= inputEnd { + if unsafe input + 1 >= inputEnd { //Leading with no room for trailing invalid = true - input += 1 + unsafe input += 1 } else { - let next = (input + 1).pointee + let next = unsafe (input + 1).pointee if !UTF16.isTrailSurrogate(next) { //Leading followed by non-trailing invalid = true - input += 1 + unsafe input += 1 } else { /* Code points outside the BMP are encoded as: @@ -553,28 +565,28 @@ private func processNonASCIIScalarFallback( scalar = utf16AstralPlaneMin + ((UInt32(cu) - utf16LeadSurrogateMin) << 10) + (UInt32(next) - utf16TrailSurrogateMin) - input += 2 + unsafe input += 2 } } } else if UTF16.isTrailSurrogate(cu) { //Trailing with no leading invalid = true - input += 1 + unsafe input += 1 } else { scalar = UInt32(cu) - input += 1 + unsafe input += 1 } if _slowPath(invalid || scalar > utf16ScalarMax) { guard repairing else { return (.invalid, repairsMade: false) } return ( - encodeScalarAsUTF8(utf16ReplacementCharacter, - output: &output, - outputEnd: outputEnd), + unsafe encodeScalarAsUTF8(utf16ReplacementCharacter, + output: &output, + outputEnd: outputEnd), repairsMade: true ) } return ( - encodeScalarAsUTF8(scalar, output: &output, outputEnd: outputEnd), + unsafe encodeScalarAsUTF8(scalar, output: &output, outputEnd: outputEnd), repairsMade: false ) } From bb2437d80c0ac6fc0386f049b0844c19af7cd0d4 Mon Sep 17 00:00:00 2001 From: David Smith Date: Tue, 29 Jul 2025 10:29:27 -0700 Subject: [PATCH 14/14] Fix precondition --- stdlib/public/core/UTF16.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/public/core/UTF16.swift b/stdlib/public/core/UTF16.swift index 15d8f1590142b..d3978ad8eae63 100644 --- a/stdlib/public/core/UTF16.swift +++ b/stdlib/public/core/UTF16.swift @@ -501,7 +501,7 @@ private func encodeScalarAsUTF8( output: inout UnsafeMutablePointer, outputEnd: UnsafePointer, ) -> ScalarFallbackResult { - _debugPrecondition(scalar > 0x80) + _debugPrecondition(scalar >= 0x80) _debugPrecondition(scalar <= utf16ScalarMax) if scalar <= utf8TwoByteMax { if unsafe output + 2 > outputEnd { return .invalid }