Skip to content

Commit ebdd00c

Browse files
Proposed design for skipping of KeyPath projections across trivially-typed memory.
1 parent f3e061f commit ebdd00c

File tree

1 file changed

+129
-24
lines changed

1 file changed

+129
-24
lines changed

stdlib/public/core/KeyPath.swift

Lines changed: 129 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ internal func _abstract(
3535
/// type.
3636
@_objcRuntimeName(_TtCs11_AnyKeyPath)
3737
public class AnyKeyPath: Hashable, _AppendKeyPath {
38+
internal var _isPureStructKeyPath: Bool?
39+
internal var _pureStructValueOffset: Int = 0
3840
/// The root type for this key path.
3941
@inlinable
4042
public static var rootType: Any.Type {
@@ -150,47 +152,110 @@ public class AnyKeyPath: Hashable, _AppendKeyPath {
150152
) -> Self {
151153
_internalInvariant(bytes > 0 && bytes % 4 == 0,
152154
"capacity must be multiple of 4 bytes")
153-
let result = Builtin.allocWithTailElems_1(self, (bytes/4)._builtinWordValue,
155+
let keypath = Builtin.allocWithTailElems_1(self, (bytes/4)._builtinWordValue,
154156
Int32.self)
155-
result._kvcKeyPathStringPtr = nil
156-
let base = UnsafeMutableRawPointer(Builtin.projectTailElems(result,
157+
keypath._kvcKeyPathStringPtr = nil
158+
let base = UnsafeMutableRawPointer(Builtin.projectTailElems(keypath,
157159
Int32.self))
158160
body(UnsafeMutableRawBufferPointer(start: base, count: bytes))
159-
return result
161+
keypath._computeOffsetForPureStructKeypath()
162+
return keypath
160163
}
161-
162164
final internal func withBuffer<T>(_ f: (KeyPathBuffer) throws -> T) rethrows -> T {
163165
defer { _fixLifetime(self) }
164166

165167
let base = UnsafeRawPointer(Builtin.projectTailElems(self, Int32.self))
166168
return try f(KeyPathBuffer(base: base))
167169
}
168170

169-
@usableFromInline // Exposed as public API by MemoryLayout<Root>.offset(of:)
170-
internal var _storedInlineOffset: Int? {
171-
return withBuffer {
172-
var buffer = $0
171+
public var isPureStructKeyPath: Bool {
172+
return _isPureStructKeyPath ?? false
173+
}
173174

174-
// The identity key path is effectively a stored keypath of type Self
175-
// at offset zero
176-
if buffer.data.isEmpty { return 0 }
175+
internal func isClass(_ item: Any.Type) -> Bool {
176+
// Displays "warning: 'is' test is always true" at compile time, but that's not actually the case.
177+
return (item is AnyObject)
178+
}
177179

178-
var offset = 0
179-
while true {
180-
let (rawComponent, optNextType) = buffer.next()
181-
switch rawComponent.header.kind {
182-
case .struct:
183-
offset += rawComponent._structOrClassOffset
180+
// TODO: Find a quicker way to see if this is a tuple.
181+
internal func isTuple(_ item: Any) -> Bool {
182+
// Unwraps type information if possible.
183+
// Otherwise, everything the Mirror sees would be "Optional<Any.Type>".
184+
func unwrapType<T>(_ any: T) -> Any {
185+
let mirror = Mirror(reflecting: any)
186+
guard mirror.displayStyle == .optional, let first = mirror.children.first else {
187+
return any
188+
}
189+
return first.value
190+
}
184191

185-
case .class, .computed, .optionalChain, .optionalForce, .optionalWrap, .external:
186-
return .none
192+
let mirror = Mirror(reflecting: unwrapType(item))
193+
let description = mirror.description
194+
let offsetOfFirstBracketForMirrorDescriptionOfTuple = 11
195+
let idx = description.index(description.startIndex, offsetBy: offsetOfFirstBracketForMirrorDescriptionOfTuple)
196+
if description[idx] == "(" {
197+
return true
198+
}
199+
return false
200+
}
201+
202+
// If this keypath traverses structs only, it'll have a predictable memory layout.
203+
// We can then jump to the value directly in _projectReadOnly().
204+
internal func _computeOffsets() -> (offset: Int, isPureStruct: Bool, isTuple: Bool) {
205+
_pureStructValueOffset = 0
206+
var isPureStruct = true
207+
var _isTuple = false
208+
defer {
209+
_isPureStructKeyPath = isPureStruct
210+
}
211+
withBuffer {
212+
var buffer = $0
213+
if buffer.data.isEmpty {
214+
if isClass(Self._rootAndValueType.root) {
215+
isPureStruct = false
216+
}
217+
} else {
218+
while true {
219+
let (rawComponent, optNextType) = buffer.next()
220+
if isTuple(optNextType as Any) {
221+
isPureStruct = false
222+
_isTuple = true
223+
}
224+
switch rawComponent.header.kind {
225+
case .struct:
226+
_pureStructValueOffset += rawComponent._structOrClassOffset
227+
case .class, .computed, .optionalChain, .optionalForce, .optionalWrap, .external:
228+
isPureStruct = false
229+
}
230+
if optNextType == nil {
231+
break
232+
}
233+
}
187234
}
235+
}
236+
return (_pureStructValueOffset, isPureStruct, _isTuple)
237+
}
238+
239+
internal func _computeOffsetForPureStructKeypath() {
240+
_ = _computeOffsets()
241+
}
188242

189-
if optNextType == nil { return .some(offset) }
243+
// This function was refactored since _computeOffsets() was performing
244+
// essentially the same computation.
245+
@usableFromInline // Exposed as public API by MemoryLayout<Root>.offset(of:)
246+
internal var _storedInlineOffset: Int? {
247+
// TODO: Cache this value in a similar manner to _pureStructValueOffset.
248+
// The current design assumes keypath read and write operations will be called
249+
// much more often than MemoryLayout<Root>.offset(of:).
250+
let offsetInformation = _computeOffsets()
251+
if offsetInformation.isPureStruct || offsetInformation.isTuple {
252+
return .some(offsetInformation.offset)
253+
} else {
254+
return .none
190255
}
191256
}
192257
}
193-
}
258+
194259

195260
/// A partially type-erased key path, from a concrete root type to any
196261
/// resulting value type.
@@ -246,6 +311,17 @@ public class KeyPath<Root, Value>: PartialKeyPath<Root> {
246311

247312
@usableFromInline
248313
internal final func _projectReadOnly(from root: Root) -> Value {
314+
315+
//One performance improvement is to skip right to Value
316+
//if this keypath traverses through structs only.
317+
if isPureStructKeyPath
318+
{
319+
return withUnsafeBytes(of: root) {
320+
let pointer = $0.baseAddress.unsafelyUnwrapped.advanced(by: _pureStructValueOffset)
321+
return pointer.assumingMemoryBound(to: Value.self).pointee
322+
}
323+
}
324+
249325
// TODO: For perf, we could use a local growable buffer instead of Any
250326
var curBase: Any = root
251327
return withBuffer {
@@ -302,6 +378,14 @@ public class WritableKeyPath<Root, Value>: KeyPath<Root, Value> {
302378
@usableFromInline
303379
internal func _projectMutableAddress(from base: UnsafePointer<Root>)
304380
-> (pointer: UnsafeMutablePointer<Value>, owner: AnyObject?) {
381+
382+
// Don't declare "p" above this if-statement; it may slow things down.
383+
if isPureStructKeyPath
384+
{
385+
let p = UnsafeRawPointer(base).advanced(by: _pureStructValueOffset)
386+
return (pointer: UnsafeMutablePointer(
387+
mutating: p.assumingMemoryBound(to: Value.self)), owner: nil)
388+
}
305389
var p = UnsafeRawPointer(base)
306390
var type: Any.Type = Root.self
307391
var keepAlive: AnyObject?
@@ -2252,6 +2336,19 @@ extension _AppendKeyPath /* where Self == ReferenceWritableKeyPath<T,U> */ {
22522336
}
22532337
}
22542338

2339+
/// Updates information pertaining to the types associated with each key path.
2340+
///
2341+
/// Note: Currently we only distinguish between keypaths that traverse only structs to get to the final value,
2342+
/// and all other types. This is done for performance reasons.
2343+
/// Other type information may be handled in the future to improve performance.
2344+
internal func _processAppendingKeyPathType(root: inout AnyKeyPath, leaf: AnyKeyPath) {
2345+
root._isPureStructKeyPath = root.isPureStructKeyPath && leaf.isPureStructKeyPath
2346+
if let isPureStruct = root._isPureStructKeyPath, isPureStruct {
2347+
root._computeOffsetForPureStructKeypath()
2348+
}
2349+
}
2350+
2351+
22552352
@usableFromInline
22562353
internal func _tryToAppendKeyPaths<Result: AnyKeyPath>(
22572354
root: AnyKeyPath,
@@ -2277,7 +2374,13 @@ internal func _tryToAppendKeyPaths<Result: AnyKeyPath>(
22772374
}
22782375
return _openExistential(rootValue, do: open2)
22792376
}
2280-
return _openExistential(rootRoot, do: open)
2377+
var returnValue:AnyKeyPath = _openExistential(rootRoot, do: open)
2378+
_processAppendingKeyPathType(root: &returnValue, leaf: leaf)
2379+
if let returnValue = returnValue as? Result
2380+
{
2381+
return returnValue
2382+
}
2383+
return nil
22812384
}
22822385

22832386
@usableFromInline
@@ -2289,7 +2392,7 @@ internal func _appendingKeyPaths<
22892392
leaf: KeyPath<Value, AppendedValue>
22902393
) -> Result {
22912394
let resultTy = type(of: root).appendedType(with: type(of: leaf))
2292-
return root.withBuffer {
2395+
var returnValue:AnyKeyPath = root.withBuffer {
22932396
var rootBuffer = $0
22942397
return leaf.withBuffer {
22952398
var leafBuffer = $0
@@ -2423,6 +2526,8 @@ internal func _appendingKeyPaths<
24232526
return unsafeDowncast(result, to: Result.self)
24242527
}
24252528
}
2529+
_processAppendingKeyPathType(root: &returnValue, leaf: leaf)
2530+
return returnValue as! Result
24262531
}
24272532

24282533
// The distance in bytes from the address point of a KeyPath object to its

0 commit comments

Comments
 (0)