Skip to content

Commit 1b56084

Browse files
committed
[5.0] [SR-7749] Poor performance of String.replacingOccurrences(of:with:) in corelibs-foundation
This supercedes swiftlang#1620. (cherry picked from commit 4cd59de) (cherry picked from commit 1e71e90)
1 parent 0f28ef1 commit 1b56084

File tree

2 files changed

+94
-8
lines changed

2 files changed

+94
-8
lines changed

Foundation/NSString.swift

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,18 @@ extension NSMutableString {
14121412
}
14131413
return 0
14141414
}
1415+
1416+
private static func makeFindResultsRangeIterator(findResults: CFArray, count: Int, backwards: Bool) -> AnyIterator<NSRange> {
1417+
var index = 0
1418+
return AnyIterator<NSRange>() { () -> NSRange? in
1419+
defer { index += 1 }
1420+
if index < count {
1421+
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? count - index - 1 : index)
1422+
return NSRange(rangePtr!.load(as: CFRange.self))
1423+
}
1424+
return nil
1425+
}
1426+
}
14151427

14161428
public func replaceOccurrences(of target: String, with replacement: String, options: CompareOptions = [], range searchRange: NSRange) -> Int {
14171429
let backwards = options.contains(.backwards)
@@ -1422,19 +1434,35 @@ extension NSMutableString {
14221434
if options.contains(.regularExpression) {
14231435
return _replaceOccurrencesOfRegularExpressionPattern(target, withTemplate:replacement, options:options, range: searchRange)
14241436
}
1425-
14261437

1427-
if let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) {
1428-
let numOccurrences = CFArrayGetCount(findResults)
1429-
for cnt in 0..<numOccurrences {
1430-
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? cnt : numOccurrences - cnt - 1)
1431-
replaceCharacters(in: NSRange(rangePtr!.load(as: CFRange.self)), with: replacement)
1438+
guard let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) else {
1439+
return 0
1440+
}
1441+
let numOccurrences = CFArrayGetCount(findResults)
1442+
1443+
let rangeIterator = NSMutableString.makeFindResultsRangeIterator(findResults: findResults, count: numOccurrences, backwards: backwards)
1444+
1445+
guard type(of: self) == NSMutableString.self else {
1446+
// If we're dealing with non NSMutableString, mutations must go through `replaceCharacters` (documented behavior)
1447+
for range in rangeIterator {
1448+
replaceCharacters(in: range, with: replacement)
14321449
}
1450+
14331451
return numOccurrences
1434-
} else {
1435-
return 0
14361452
}
14371453

1454+
var newStorage = Substring()
1455+
var sourceStringCurrentIndex = _storage.startIndex
1456+
for range in rangeIterator {
1457+
let matchStartIndex = String.Index(utf16Offset: range.location, in: _storage)
1458+
let matchEndIndex = String.Index(utf16Offset: range.location + range.length, in: _storage)
1459+
newStorage += _storage[sourceStringCurrentIndex ..< matchStartIndex]
1460+
newStorage += replacement
1461+
sourceStringCurrentIndex = matchEndIndex
1462+
}
1463+
newStorage += _storage[sourceStringCurrentIndex ..< _storage.endIndex]
1464+
_storage = String(newStorage)
1465+
return numOccurrences
14381466
}
14391467

14401468
public func applyTransform(_ transform: String, reverse: Bool, range: NSRange, updatedRange resultingRange: NSRangePointer?) -> Bool {

TestFoundation/TestNSString.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,4 +1417,62 @@ extension TestNSString {
14171417
XCTAssertEqual(str4.replacingOccurrences(of: "\r\n", with: "\r\n"), "Hello\r\rworld.")
14181418
XCTAssertEqual(str4.replacingOccurrences(of: "\n\r", with: " "), "Hello\r\rworld.")
14191419
}
1420+
1421+
func test_replacingOccurrencesInSubclass() {
1422+
class TestMutableString: NSMutableString {
1423+
private var wrapped: NSMutableString
1424+
var replaceCharactersCount: Int = 0
1425+
1426+
override var length: Int {
1427+
return wrapped.length
1428+
}
1429+
1430+
override func character(at index: Int) -> unichar {
1431+
return wrapped.character(at: index)
1432+
}
1433+
1434+
override func replaceCharacters(in range: NSRange, with aString: String) {
1435+
defer { replaceCharactersCount += 1 }
1436+
wrapped.replaceCharacters(in: range, with: aString)
1437+
}
1438+
1439+
override func mutableCopy(with zone: NSZone? = nil) -> Any {
1440+
return wrapped.mutableCopy()
1441+
}
1442+
1443+
required init(stringLiteral value: StaticString) {
1444+
wrapped = .init(stringLiteral: value)
1445+
super.init(stringLiteral: value)
1446+
}
1447+
1448+
required init(capacity: Int) {
1449+
fatalError("init(capacity:) has not been implemented")
1450+
}
1451+
1452+
required init(string aString: String) {
1453+
fatalError("init(string:) has not been implemented")
1454+
}
1455+
1456+
required convenience init?(coder aDecoder: NSCoder) {
1457+
fatalError("init(coder:) has not been implemented")
1458+
}
1459+
1460+
required init(characters: UnsafePointer<unichar>, length: Int) {
1461+
fatalError("init(characters:length:) has not been implemented")
1462+
}
1463+
1464+
required convenience init(extendedGraphemeClusterLiteral value: StaticString) {
1465+
fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented")
1466+
}
1467+
1468+
required convenience init(unicodeScalarLiteral value: StaticString) {
1469+
fatalError("init(unicodeScalarLiteral:) has not been implemented")
1470+
}
1471+
}
1472+
1473+
let testString = TestMutableString(stringLiteral: "ababab")
1474+
XCTAssertEqual(testString.replacingOccurrences(of: "ab", with: "xx"), "xxxxxx")
1475+
XCTAssertEqual(testString.replaceCharactersCount, 3)
1476+
}
1477+
14201478
}

0 commit comments

Comments
 (0)