Skip to content

Commit 9464305

Browse files
committed
[SR-7749] Poor performance of String.replacingOccurrences(of:with:) in corelibs-foundation
This supercedes swiftlang#1620.
1 parent 56934bf commit 9464305

File tree

2 files changed

+86
-9
lines changed

2 files changed

+86
-9
lines changed

Foundation/NSString.swift

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,7 @@ extension NSString {
11301130
}
11311131
return ""
11321132
}
1133-
1133+
11341134
open func replacingOccurrences(of target: String, with replacement: String, options: CompareOptions = [], range searchRange: NSRange) -> String {
11351135
if options.contains(.regularExpression) {
11361136
return _stringByReplacingOccurrencesOfRegularExpressionPattern(target, withTemplate: replacement, options: options, range: searchRange)
@@ -1462,19 +1462,38 @@ extension NSMutableString {
14621462
if options.contains(.regularExpression) {
14631463
return _replaceOccurrencesOfRegularExpressionPattern(target, withTemplate:replacement, options:options, range: searchRange)
14641464
}
1465-
14661465

1467-
if let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) {
1468-
let numOccurrences = CFArrayGetCount(findResults)
1466+
guard let findResults = CFStringCreateArrayWithFindResults(kCFAllocatorSystemDefault, _cfObject, target._cfObject, CFRange(searchRange), options._cfValue(true)) else {
1467+
return 0
1468+
}
1469+
let numOccurrences = CFArrayGetCount(findResults)
1470+
1471+
return withExtendedLifetime(findResults) {
1472+
guard type(of: self) == NSMutableString.self else {
1473+
// If we're dealing with non NSMutableString, mutations must go through `replaceCharacters` (documented behavior)
1474+
for cnt in 0..<numOccurrences {
1475+
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? cnt : numOccurrences - cnt - 1)
1476+
replaceCharacters(in: NSRange(rangePtr!.load(as: CFRange.self)), with: replacement)
1477+
}
1478+
1479+
return numOccurrences
1480+
}
1481+
1482+
var newStorage = Substring()
1483+
var sourceStringCurrentIndex = _storage.startIndex
14691484
for cnt in 0..<numOccurrences {
1470-
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? cnt : numOccurrences - cnt - 1)
1471-
replaceCharacters(in: NSRange(rangePtr!.load(as: CFRange.self)), with: replacement)
1485+
let rangePtr = CFArrayGetValueAtIndex(findResults, backwards ? numOccurrences - cnt - 1 : cnt)
1486+
let range = NSRange(rangePtr!.load(as: CFRange.self))
1487+
let matchStartIndex = String.Index(utf16Offset: range.location, in: _storage)
1488+
let matchEndIndex = String.Index(utf16Offset: range.location + range.length, in: _storage)
1489+
newStorage += _storage[sourceStringCurrentIndex..<matchStartIndex]
1490+
newStorage += replacement
1491+
sourceStringCurrentIndex = matchEndIndex
14721492
}
1493+
newStorage += _storage[sourceStringCurrentIndex ..< _storage.endIndex]
1494+
_storage = String(newStorage)
14731495
return numOccurrences
1474-
} else {
1475-
return 0
14761496
}
1477-
14781497
}
14791498

14801499
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
@@ -1413,6 +1413,64 @@ extension TestNSString {
14131413
XCTAssertEqual(str4.replacingOccurrences(of: "\n\r", with: " "), "Hello\r\rworld.")
14141414
}
14151415

1416+
func test_replacingOccurrencesInSubclass() {
1417+
class TestMutableString: NSMutableString {
1418+
private var wrapped: NSMutableString
1419+
var replaceCharactersCount: Int = 0
1420+
1421+
override var length: Int {
1422+
return wrapped.length
1423+
}
1424+
1425+
override func character(at index: Int) -> unichar {
1426+
return wrapped.character(at: index)
1427+
}
1428+
1429+
override func replaceCharacters(in range: NSRange, with aString: String) {
1430+
defer { replaceCharactersCount += 1 }
1431+
wrapped.replaceCharacters(in: range, with: aString)
1432+
}
1433+
1434+
override func mutableCopy(with zone: NSZone? = nil) -> Any {
1435+
return wrapped.mutableCopy()
1436+
}
1437+
1438+
required init(stringLiteral value: StaticString) {
1439+
wrapped = .init(stringLiteral: value)
1440+
super.init(stringLiteral: value)
1441+
}
1442+
1443+
required init(capacity: Int) {
1444+
fatalError("init(capacity:) has not been implemented")
1445+
}
1446+
1447+
required init(string aString: String) {
1448+
fatalError("init(string:) has not been implemented")
1449+
}
1450+
1451+
required convenience init?(coder aDecoder: NSCoder) {
1452+
fatalError("init(coder:) has not been implemented")
1453+
}
1454+
1455+
required init(characters: UnsafePointer<unichar>, length: Int) {
1456+
fatalError("init(characters:length:) has not been implemented")
1457+
}
1458+
1459+
required convenience init(extendedGraphemeClusterLiteral value: StaticString) {
1460+
fatalError("init(extendedGraphemeClusterLiteral:) has not been implemented")
1461+
}
1462+
1463+
required convenience init(unicodeScalarLiteral value: StaticString) {
1464+
fatalError("init(unicodeScalarLiteral:) has not been implemented")
1465+
}
1466+
}
1467+
1468+
let testString = TestMutableString(stringLiteral: "ababab")
1469+
XCTAssertEqual(testString.replacingOccurrences(of: "ab", with: "xx"), "xxxxxx")
1470+
XCTAssertEqual(testString.replaceCharactersCount, 3)
1471+
}
1472+
1473+
14161474
func test_fileSystemRepresentation() {
14171475
let name = "" as NSString
14181476
let result = name.fileSystemRepresentation

0 commit comments

Comments
 (0)