Skip to content

Commit 7fb2cce

Browse files
committed
Add a method to _NSContiguousString to facilitate stack promotion
Use it for hashing and comparison. During String's hashValue and comparison function we create a _NSContiguousString instance to call Foundation's hash/compare function. This is expensive because we have allocate and deallocate a short lived object on the heap (and deallocation for Swift objects is expensive). Instead help the optimizer to allocate this object on the stack. Introduces two functions on the internal _NSContiguousString: _unsafeWithNotEscapedSelfPointer and _unsafeWithNotEscapedSelfPointerPair that pass the _NSContiguousString instance as an opaque pointer to their closure argument. Usage of these functions asserts that the closure will not escape objects transitively reachable from the opaque pointer. We then use those functions to call into the runtime to call foundation functions on the passed strings. The optimizer can promote the strings to the stack because of the assertion this API makes. let lhsStr = _NSContiguousString(self._core) // will be promoted to the stack. let rhsStr = _NSContiguousString(rhs._core) // will be promoted to the stack. let res = lhsStr._unsafeWithNotEscapedSelfPointerPair(rhsStr) { return _stdlib_compareNSStringDeterministicUnicodeCollationPointer($0, $1) } Tested by existing String tests. We should see some nice performance improvements for string comparison and dictionary benchmarks. Here is what I measured at -O on my machine Name Speedup Dictionary 2.00x Dictionary2 1.45x Dictionary2OfObjects 1.20x Dictionary3 1.50x Dictionary3OfObjects 1.45x DictionaryOfObjects 1.40x SuperChars 1.60x rdar://22173647
1 parent 9e9c80e commit 7fb2cce

File tree

6 files changed

+151
-32
lines changed

6 files changed

+151
-32
lines changed

lib/SILOptimizer/Analysis/EscapeAnalysis.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,36 @@ void EscapeAnalysis::analyzeInstruction(SILInstruction *I,
11731173
break;
11741174
}
11751175

1176+
if (FAS.getReferencedFunction() &&
1177+
FAS.getReferencedFunction()->hasSemanticsAttr(
1178+
"self_no_escaping_closure") &&
1179+
((FAS.hasIndirectResults() && FAS.getNumArguments() == 3) ||
1180+
(!FAS.hasIndirectResults() && FAS.getNumArguments() == 2)) &&
1181+
FAS.hasSelfArgument()) {
1182+
// The programmer has guaranteed that the closure will not capture the
1183+
// self pointer passed to it or anything that is transitively reachable
1184+
// from the pointer.
1185+
auto Args = FAS.getArgumentsWithoutIndirectResults();
1186+
// The first not indirect result argument is the closure.
1187+
setEscapesGlobal(ConGraph, Args[0]);
1188+
return;
1189+
}
1190+
1191+
if (FAS.getReferencedFunction() &&
1192+
FAS.getReferencedFunction()->hasSemanticsAttr(
1193+
"pair_no_escaping_closure") &&
1194+
((FAS.hasIndirectResults() && FAS.getNumArguments() == 4) ||
1195+
(!FAS.hasIndirectResults() && FAS.getNumArguments() == 3)) &&
1196+
FAS.hasSelfArgument()) {
1197+
// The programmer has guaranteed that the closure will not capture the
1198+
// self pointer passed to it or anything that is transitively reachable
1199+
// from the pointer.
1200+
auto Args = FAS.getArgumentsWithoutIndirectResults();
1201+
// The second not indirect result argument is the closure.
1202+
setEscapesGlobal(ConGraph, Args[1]);
1203+
return;
1204+
}
1205+
11761206
if (RecursionDepth < MaxRecursionDepth) {
11771207
CalleeList Callees = BCA->getCalleeList(FAS);
11781208
if (Callees.allCalleesVisible()) {

stdlib/public/core/String.swift

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,11 @@ extension String {
314314
public func _stdlib_compareNSStringDeterministicUnicodeCollation(
315315
lhs: AnyObject, _ rhs: AnyObject
316316
) -> Int32
317+
318+
@_silgen_name("swift_stdlib_compareNSStringDeterministicUnicodeCollationPtr")
319+
public func _stdlib_compareNSStringDeterministicUnicodeCollationPointer(
320+
lhs: OpaquePointer, _ rhs: OpaquePointer
321+
) -> Int32
317322
#endif
318323

319324
extension String : Equatable {
@@ -372,6 +377,15 @@ extension String {
372377
// Note: this operation should be consistent with equality comparison of
373378
// Character.
374379
#if _runtime(_ObjC)
380+
if self._core.hasContiguousStorage && rhs._core.hasContiguousStorage {
381+
let lhsStr = _NSContiguousString(self._core)
382+
let rhsStr = _NSContiguousString(rhs._core)
383+
let res = lhsStr._unsafeWithNotEscapedSelfPointerPair(rhsStr) {
384+
return Int(
385+
_stdlib_compareNSStringDeterministicUnicodeCollationPointer($0, $1))
386+
}
387+
return res
388+
}
375389
return Int(_stdlib_compareNSStringDeterministicUnicodeCollation(
376390
_bridgeToObjectiveCImpl(), rhs._bridgeToObjectiveCImpl()))
377391
#else
@@ -445,12 +459,12 @@ extension String {
445459

446460
#if _runtime(_ObjC)
447461
@warn_unused_result
448-
@_silgen_name("swift_stdlib_NSStringNFDHashValue")
449-
func _stdlib_NSStringNFDHashValue(str: AnyObject) -> Int
462+
@_silgen_name("swift_stdlib_NSStringHashValue")
463+
func _stdlib_NSStringHashValue(str: AnyObject, _ isASCII: Bool) -> Int
450464

451465
@warn_unused_result
452-
@_silgen_name("swift_stdlib_NSStringASCIIHashValue")
453-
func _stdlib_NSStringASCIIHashValue(str: AnyObject) -> Int
466+
@_silgen_name("swift_stdlib_NSStringHashValuePointer")
467+
func _stdlib_NSStringHashValuePointer(str: OpaquePointer, _ isASCII: Bool) -> Int
454468
#endif
455469

456470
extension String : Hashable {
@@ -470,16 +484,18 @@ extension String : Hashable {
470484
#else
471485
let hashOffset = Int(bitPattern: 0x429b_1266_88dd_cc21)
472486
#endif
473-
// FIXME(performance): constructing a temporary NSString is extremely
474-
// wasteful and inefficient.
475-
let cocoaString = unsafeBitCast(
476-
self._bridgeToObjectiveCImpl(), to: _NSStringCore.self)
477-
478-
// If we have an ASCII string, we do not need to normalize.
479-
if self._core.isASCII {
480-
return hashOffset ^ _stdlib_NSStringASCIIHashValue(cocoaString)
487+
// If we have a contigous string then we can use the stack optimization.
488+
let core = self._core
489+
let isASCII = core.isASCII
490+
if core.hasContiguousStorage {
491+
let stackAllocated = _NSContiguousString(core)
492+
return hashOffset ^ stackAllocated._unsafeWithNotEscapedSelfPointer {
493+
return _stdlib_NSStringHashValuePointer($0, isASCII )
494+
}
481495
} else {
482-
return hashOffset ^ _stdlib_NSStringNFDHashValue(cocoaString)
496+
let cocoaString = unsafeBitCast(
497+
self._bridgeToObjectiveCImpl(), to: _NSStringCore.self)
498+
return hashOffset ^ _stdlib_NSStringHashValue(cocoaString, isASCII)
483499
}
484500
#else
485501
if self._core.isASCII {

stdlib/public/core/StringBridge.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,39 @@ public final class _NSContiguousString : _SwiftNativeNSString {
269269
return self
270270
}
271271

272+
/// The caller of this function guarantees that the closure 'body' does not
273+
/// escape the object referenced by the opaque pointer passed to it or
274+
/// anything transitively reachable form this object. Doing so
275+
/// will result in undefined behavior.
276+
@_semantics("self_no_escaping_closure")
277+
func _unsafeWithNotEscapedSelfPointer<Result>(
278+
@noescape body: (OpaquePointer) throws -> Result
279+
) rethrows -> Result {
280+
let selfAsPointer = unsafeBitCast(self, to: OpaquePointer.self)
281+
defer {
282+
_fixLifetime(self)
283+
}
284+
return try body(selfAsPointer)
285+
}
286+
287+
/// The caller of this function guarantees that the closure 'body' does not
288+
/// escape either object referenced by the opaque pointer pair passed to it or
289+
/// transitively reachable objects. Doing so will result in undefined
290+
/// behavior.
291+
@_semantics("pair_no_escaping_closure")
292+
func _unsafeWithNotEscapedSelfPointerPair<Result>(
293+
rhs: _NSContiguousString,
294+
@noescape body: (OpaquePointer, OpaquePointer) throws -> Result
295+
) rethrows -> Result {
296+
let selfAsPointer = unsafeBitCast(self, to: OpaquePointer.self)
297+
let rhsAsPointer = unsafeBitCast(rhs, to: OpaquePointer.self)
298+
defer {
299+
_fixLifetime(self)
300+
_fixLifetime(rhs)
301+
}
302+
return try body(selfAsPointer, rhsAsPointer)
303+
}
304+
272305
public let _core: _StringCore
273306
}
274307

stdlib/public/stubs/SwiftNativeNSXXXBase.mm.gyb

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,22 +117,37 @@ extern "C" int32_t swift_stdlib_compareNSStringDeterministicUnicodeCollation(
117117
}
118118

119119
SWIFT_RUNTIME_STDLIB_INTERFACE
120-
extern "C" size_t
121-
swift_stdlib_NSStringNFDHashValue(NSString *NS_RELEASES_ARGUMENT str) {
122-
size_t Result = str.decomposedStringWithCanonicalMapping.hash;
123-
swift_unknownRelease(str);
120+
extern "C" int32_t
121+
swift_stdlib_compareNSStringDeterministicUnicodeCollationPtr(void *Lhs,
122+
void *Rhs) {
123+
NSString *lhs = (NSString *)Lhs;
124+
NSString *rhs = (NSString *)Rhs;
125+
126+
// 'kCFCompareNonliteral' actually means "normalize to NFD".
127+
int Result = CFStringCompare((__bridge CFStringRef)lhs,
128+
(__bridge CFStringRef)rhs, kCFCompareNonliteral);
124129
return Result;
125130
}
126131

127-
// For strings we know only have ASCII
128132
SWIFT_RUNTIME_STDLIB_INTERFACE
129133
extern "C" size_t
130-
swift_stdlib_NSStringASCIIHashValue(NSString *NS_RELEASES_ARGUMENT str) {
131-
size_t Result = str.hash;
134+
swift_stdlib_NSStringHashValue(NSString *NS_RELEASES_ARGUMENT str,
135+
bool isASCII) {
136+
size_t Result =
137+
isASCII ? str.hash : str.decomposedStringWithCanonicalMapping.hash;
138+
132139
swift_unknownRelease(str);
133140
return Result;
134141
}
135142

143+
SWIFT_RUNTIME_STDLIB_INTERFACE
144+
extern "C" size_t
145+
swift_stdlib_NSStringHashValuePointer(void *opaque, bool isASCII) {
146+
NSString *str = (NSString *)opaque;
147+
return isASCII ? str.hash : str.decomposedStringWithCanonicalMapping.hash;
148+
}
149+
150+
136151
SWIFT_RUNTIME_STDLIB_INTERFACE
137152
extern "C" bool swift_stdlib_NSStringHasPrefixNFD(NSString *theString,
138153
NSString *prefix) {

test/1_stdlib/RuntimeObjC.swift

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -543,22 +543,12 @@ RuntimeFoundationWrappers.test(
543543
expectEqual(0, nsStringCanaryCount)
544544
}
545545

546-
RuntimeFoundationWrappers.test("_stdlib_NSStringNFDHashValue/NoLeak") {
546+
RuntimeFoundationWrappers.test("_stdlib_NSStringHashValue/NoLeak") {
547547
nsStringCanaryCount = 0
548548
autoreleasepool {
549549
let a = NSStringCanary()
550550
expectEqual(1, nsStringCanaryCount)
551-
_stdlib_NSStringNFDHashValue(a)
552-
}
553-
expectEqual(0, nsStringCanaryCount)
554-
}
555-
556-
RuntimeFoundationWrappers.test("_stdlib_NSStringASCIIHashValue/NoLeak") {
557-
nsStringCanaryCount = 0
558-
autoreleasepool {
559-
let a = NSStringCanary()
560-
expectEqual(1, nsStringCanaryCount)
561-
_stdlib_NSStringASCIIHashValue(a)
551+
_stdlib_NSStringHashValue(a, true)
562552
}
563553
expectEqual(0, nsStringCanaryCount)
564554
}

test/SILOptimizer/escape_analysis.sil

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,3 +1214,38 @@ sil [_semantics "array.get_element_address"] @get_element_address : $@convention
12141214
sil [_semantics "array.get_count"] @get_count : $@convention(method) (@guaranteed Array<X>) -> Int32
12151215
sil [_semantics "array.get_capacity"] @get_capacity : $@convention(method) (@guaranteed Array<X>) -> Int32
12161216

1217+
sil [_semantics "pair_no_escaping_closure"] @unsafeWithNotEscapedSelfPointerPair : $@convention(method) (@owned X, @owned @callee_owned (X, X) -> (@out X, @error ErrorType), @guaranteed X) -> (@out X, @error ErrorType)
1218+
sil [_semantics "self_no_escaping_closure"] @unsafeWithNotEscapedSelfPointer: $@convention(method) (@owned @callee_owned (X, X) -> (@out X, @error ErrorType), @guaranteed X) -> (@out X, @error ErrorType)
1219+
1220+
// CHECK-LABEL: CG of semantics_pair_no_escaping_closure
1221+
// CHECK-NEXT: Arg %0 Esc: A, Succ:
1222+
// CHECK-NEXT: Arg %1 Esc: A, Succ:
1223+
// CHECK-NEXT: Arg %2 Esc: G, Succ: (%2.1)
1224+
// CHECK-NEXT: Con %2.1 Esc: G, Succ:
1225+
// CHECK-NEXT: Val %4 Esc: %5, Succ:
1226+
// CHECK-NEXT: End
1227+
sil @semantics_pair_no_escaping_closure : $@convention(thin) (@owned X, @guaranteed X, @owned @callee_owned (X, X) -> (@out X, @error ErrorType)) -> () {
1228+
bb(%0 : $X, %1 : $X, %2: $@callee_owned (X, X) -> (@out X, @error ErrorType)):
1229+
%3 = function_ref @unsafeWithNotEscapedSelfPointerPair : $@convention(method) (@owned X, @owned @callee_owned (X, X) -> (@out X, @error ErrorType), @guaranteed X) -> (@out X, @error ErrorType)
1230+
%4 = alloc_stack $X
1231+
%6 = apply [nothrow] %3(%4, %0, %2, %1) : $@convention(method) (@owned X, @owned @callee_owned (X, X) -> (@out X, @error ErrorType), @guaranteed X) -> (@out X, @error ErrorType)
1232+
dealloc_stack %4 : $*X
1233+
%7 = tuple()
1234+
return %7 : $()
1235+
}
1236+
1237+
// CHECK-LABEL: CG of semantics_self_no_escaping_closure
1238+
// CHECK-NEXT: Arg %0 Esc: A, Succ:
1239+
// CHECK-NEXT: Arg %1 Esc: G, Succ: (%1.1)
1240+
// CHECK-NEXT: Con %1.1 Esc: G, Succ:
1241+
// CHECK-NEXT: Val %3 Esc: %4, Succ:
1242+
// CHECK-NEXT: End
1243+
sil @semantics_self_no_escaping_closure : $@convention(thin) (@guaranteed X, @owned @callee_owned (X, X) -> (@out X, @error ErrorType)) -> () {
1244+
bb(%0 : $X, %1: $@callee_owned (X, X) -> (@out X, @error ErrorType)):
1245+
%2 = function_ref @unsafeWithNotEscapedSelfPointer : $@convention(method) (@owned @callee_owned (X, X) -> (@out X, @error ErrorType), @guaranteed X) -> (@out X, @error ErrorType)
1246+
%3 = alloc_stack $X
1247+
%6 = apply [nothrow] %2(%3, %1, %0) : $@convention(method) (@owned @callee_owned (X, X) -> (@out X, @error ErrorType), @guaranteed X) -> (@out X, @error ErrorType)
1248+
dealloc_stack %3 : $*X
1249+
%7 = tuple()
1250+
return %7 : $()
1251+
}

0 commit comments

Comments
 (0)