AttributedContainer.init Performance Issues

Current state of AttributedContainer.init Performance

My team and I have faced unexpected degradation when performed migration to UIButton.Configuration with a AttributedString. At first, I've logged the duration locally and reproduced it in Release builds on iOS simulator and on real device.

Here is screenshot from Xcode Instruments Time Profiler:

Then we've measured this code in production (on DispatchQueue.global(qos: .default)):

func configure() {
    var warmup = UIButton.Configuration.plain()
    var container = AttributeContainer()
    container.font = UIFont.systemFont(ofSize: 12)
    warmup.attributedTitle = AttributedString("w", attributes: container)
}
Percentile Duration
p10 44 ms
p25 50 ms
p50 66 ms
p75 100 ms
p90 200 ms

So even on top-notch hardware it still takes significant amount of time - about 50 ms.

Why it is so slow

Main reasons are checks that type conforms to AttributedStringKey (here),MarkdownDecodableAttributedStringKey (here) and to AttributeScope (here).

The first conformance check for given type and protocol are mostly slow for any given (type, protocol), because to create existential container we need protocol witness table. To find protocol witness table, we need to find matching protocol conformance description, to find it the runtime must scan all protocol conformance type descriptors. Then it gets cached into ConcurrentReadableHashMap, that's why only first conformance check is expensive. That's why existing benchmark doesn't really cover this performance bottleneck.

So as an application grows and accumulates more protocol conformance descriptors, the cost of the first conformance checks increases proportionally. For instance, our app binary has more than 205k protocol conformance descriptors.

Optimization idea #1

So if protocol cast is expensive, we can introduce fast-circuit checks for all types that conform these types inside FoundationEssentials module. Because AttributeStringKey, MarkdownDecodableAttributedStringKey and AttributedScope are public, we'll be forced to fallback to original casts if our fast-circuit checks failed. Unfortunately, we're unable to make the same optimisation for types from UIKitCore, SwiftUICore, Accessibility frameworks, that are loaded dynamically in _loadDefaultAttributes, but still, we can significantly reduce overhead.

Draft changes for idea #1

First: build a dictionary with all known types for fast-circuit:
fileprivate enum _KnownFieldKind : Sendable {
    case attribute(any AttributedStringKey.Type)
    case markdownAttribute(any MarkdownDecodableAttributedStringKey.Type)
    case scope(any AttributeScope.Type)
}

fileprivate let _knownFieldKinds: [ObjectIdentifier : _KnownFieldKind] = {
    typealias F = AttributeScopes.FoundationAttributes
    let attributes: [any AttributedStringKey.Type] = [
        F.LinkAttribute.self,
        F.PersonNameComponentAttribute.self,
        F.DateFieldAttribute.self,
        F.AlternateDescriptionAttribute.self,
        F.ImageURLAttribute.self,
        F.ReplacementIndexAttribute.self,
        F.MeasurementAttribute.self,
        F.ByteCountAttribute.self,
        F.DurationFieldAttribute.self,
        F.WritingDirectionAttribute.self,
        F.InlinePresentationIntentAttribute.self,
        F.PresentationIntentAttribute.self,
        F.MarkdownSourcePositionAttribute.self,
        F.ListItemDelimiterAttribute.self,
        F.NumberFormatAttributes.SymbolAttribute.self,
        F.NumberFormatAttributes.NumberPartAttribute.self
    ]
    let markdownAttributes: [any MarkdownDecodableAttributedStringKey.Type] = [
        F.LanguageIdentifierAttribute.self,
        F.AgreementConceptAttribute.self,
        F.AgreementArgumentAttribute.self,
        F.ReferentConceptAttribute.self,
        F.LocalizedNumberFormatAttribute.self,
        F.InflectionAlternativeAttribute.self,
        F.MorphologyAttribute.self,
        F.InflectionRuleAttribute.self,
        F.AssumedFallbackInflectionAttribute.self
    ]
    let scopes: [any AttributeScope.Type] = [
        F.self,
        F.NumberFormatAttributes.self
    ]

    var table = [ObjectIdentifier : _KnownFieldKind]()
    for type in attributes { table[ObjectIdentifier(type)] = .attribute(type) }
    for type in markdownAttributes { table[ObjectIdentifier(type)] = .markdownAttribute(type) }
    for type in scopes { table[ObjectIdentifier(type)] = .scope(type) }
    return table
}()
Second: use it
for field in Type(Self.self).fields {
    let fieldType = field.type.swiftType
    // fast-circuit
    if let known = _knownFieldKinds[ObjectIdentifier(fieldType)] {
        switch known {
        case .attribute(let attribute):
            desc.attributes[attribute.name] = attribute
        case .markdownAttribute(let attribute):
            desc.attributes[attribute.name] = attribute
            desc.markdownAttributes[attribute.markdownName] = attribute
        case .scope(let scope):
            desc.merge(scope.scopeDescription)
        }
        continue
    }
    switch fieldType {
    case let attribute as any AttributedStringKey.Type:
        desc.attributes[attribute.name] = attribute
        if let markdownAttribute = attribute as? any MarkdownDecodableAttributedStringKey.Type {
            desc.markdownAttributes[markdownAttribute.markdownName] = markdownAttribute
        }
    case let scope as any AttributeScope.Type:
        desc.merge(scope.scopeDescription)
    default: break
    }
}

Optimization idea #2

internal static func markdownKeyTypes() is actually never used in this repo. But we compute data for it sacrificing performance while building markdownAttributes here:

if let markdownAttribute = attribute as? any MarkdownDecodableAttributedStringKey.Type {
    desc.markdownAttributes[markdownAttribute.markdownName] = markdownAttribute
}

If markdownKeyTypes() isn't used at all, we can just delete it. If it is used somehow internally but not actually needed along with attributeKeyTypes (that is used in _loadDefaultAttributes), we can calculate it when we really need it. If all these assumptions are wrong - we can leave it as it is.

Draft changes for idea #2

internal static func markdownKeyTypes() -> [String : any MarkdownDecodableAttributedStringKey.Type] {
    let desc = Self.scopeDescription
    if let markdownAttributes = desc.markdownAttributes {
        return markdownAttributes
    }
    
    var markdownAttributes = [String : any MarkdownDecodableAttributedStringKey.Type]()
    for attribute in desc.orderedAttributes {
        if let markdownAttribute = _markdownAttribute(attribute) {
            markdownAttributes[markdownAttribute.markdownName] = markdownAttribute
        }
    }
    let _markdownAttributes = markdownAttributes
    _loadedScopeCache.withLock {
        $0[Self.self]?.markdownAttributes = _markdownAttributes
    }
    return markdownAttributes
}

Conclusion

Sadly, optimizations here are strongly limited by dynamic loading AttributeScopes from UIKitCore, SwiftUICore and Accessibility frameworks, because described short-circuit logic can't be applied to types from these libraries.

But by using ideas from this topic it is possible to reduce overhead that almost each app is facing on main thread by approx 20-40%.

Thanks for bringing this up! I can't speak to the UIKit usage of this API (and would recommend you file a feedback to Apple about that usage if you find it problematic since that's outside the scope of the Swift forums), but focusing on the Foundation implementations you mention in the general usage sense:

We could do this, but I'd be curious to better understand how much of a performance benefit this gives us (in contrast to the increased maintenance burden of listing all of the Foundation attributes in multiple places). The Foundation attributes are certainly very commonly used, but non-Foundation attributes from AppKit, UIKit, SwiftUI, or even third party apps are also certainly very common and I don't know offhand the distribution of their usage. We can look into whether this is worthwhile, but I have a suspicion that this may not have a large effect since it would only affect a single scope and this information is already cached and only calculated on first use.

It's not used in swift-foundation because swift-foundation does not yet have the AttributedString(markdown:) API. Once we port that API to swift-foundation, it would use the markdown keys. It's currently used by the Foundation.framework build of the project and cannot be deleted. We could choose to calculate it when first calling AttributedString(markdown:), but given that this is only calculated once on first use, I wonder if iterating the scope a second time to perform those casts lazily would actually be less performant, especially for common scopes like Foundation/SwiftUI that will almost certainly be used in both APIs.

Can you elaborate a bit on how you measured this amount and what benchmarks you used?

1 Like

Hi. Jeremy! Thanks for quick response!

Can you elaborate a bit on how you measured this amount and what benchmarks you used?

This is a bit tricky part. I haven't conduct direct measurements, I've got this approximation by setting symbolic breakpoint to `swift_conformsToProtocolMaybeInstantiateSuperclasses` and printed out its arguments like pairs of (type, protocol).

Here they are, as a md table:

Casting history
# Type Protocol Result Optimized by type check depth Asked by
1 AttributeScopes.UIKitAttributes.FontAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
2 AttributeScopes.UIKitAttributes.ParagraphStyleAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
3 AttributeScopes.UIKitAttributes.ParagraphStyleAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
4 AttributeScopes.UIKitAttributes.ForegroundColorAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
5 AttributeScopes.UIKitAttributes.ForegroundColorAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
6 AttributeScopes.UIKitAttributes.BackgroundColorAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
7 AttributeScopes.UIKitAttributes.BackgroundColorAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
8 AttributeScopes.UIKitAttributes.LigatureAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
9 AttributeScopes.UIKitAttributes.LigatureAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
10 AttributeScopes.UIKitAttributes.KernAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
11 AttributeScopes.UIKitAttributes.KernAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
12 AttributeScopes.UIKitAttributes.TrackingAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
13 AttributeScopes.UIKitAttributes.TrackingAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
14 AttributeScopes.UIKitAttributes.StrikethroughStyleAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
15 AttributeScopes.UIKitAttributes.StrikethroughStyleAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
16 AttributeScopes.UIKitAttributes.UnderlineStyleAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
17 AttributeScopes.UIKitAttributes.UnderlineStyleAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
18 AttributeScopes.UIKitAttributes.StrokeColorAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
19 AttributeScopes.UIKitAttributes.StrokeColorAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
20 AttributeScopes.UIKitAttributes.StrokeWidthAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
21 AttributeScopes.UIKitAttributes.StrokeWidthAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
22 AttributeScopes.UIKitAttributes.ShadowAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
23 AttributeScopes.UIKitAttributes.ShadowAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
24 AttributeScopes.UIKitAttributes.TextEffectAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
25 AttributeScopes.UIKitAttributes.TextEffectAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
26 AttributeScopes.UIKitAttributes.BaselineOffsetAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
27 AttributeScopes.UIKitAttributes.BaselineOffsetAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
28 AttributeScopes.UIKitAttributes.UnderlineColorAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
29 AttributeScopes.UIKitAttributes.UnderlineColorAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
30 AttributeScopes.UIKitAttributes.StrikethroughColorAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
31 AttributeScopes.UIKitAttributes.StrikethroughColorAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
32 AttributeScopes.UIKitAttributes.AttachmentAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
33 AttributeScopes.UIKitAttributes.AttachmentAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
34 AttributeScopes.UIKitAttributes.ObliquenessAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
35 AttributeScopes.UIKitAttributes.ObliquenessAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
36 AttributeScopes.UIKitAttributes.ExpansionAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
37 AttributeScopes.UIKitAttributes.ExpansionAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
38 AttributeScopes.UIKitAttributes.TextItemTagAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
39 AttributeScopes.UIKitAttributes.TextItemTagAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
40 AttributeScopes.UIKitAttributes.AdaptiveImageGlyphAttribute AttributedStringKey YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
41 AttributeScopes.UIKitAttributes.AdaptiveImageGlyphAttribute MarkdownDecodableAttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
42 AttributeScopes.AccessibilityAttributes AttributedStringKey no false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
43 AttributeScopes.AccessibilityAttributes AttributeScope YES false 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
44 AttributeScopes.AccessibilityAttributes.HeadingLevelAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
45 AttributeScopes.AccessibilityAttributes.HeadingLevelAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
46 AttributeScopes.AccessibilityAttributes.TextCustomAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
47 AttributeScopes.AccessibilityAttributes.TextCustomAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
48 AttributeScopes.AccessibilityAttributes.TextualContextAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
49 AttributeScopes.AccessibilityAttributes.TextualContextAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
50 AttributeScopes.AccessibilityAttributes.IncludesPunctuationAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
51 AttributeScopes.AccessibilityAttributes.IncludesPunctuationAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
52 AttributeScopes.AccessibilityAttributes.AdjustedPitchAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
53 AttributeScopes.AccessibilityAttributes.AdjustedPitchAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
54 AttributeScopes.AccessibilityAttributes.SpellOutAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
55 AttributeScopes.AccessibilityAttributes.SpellOutAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
56 AttributeScopes.AccessibilityAttributes.QueueAnnouncementAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
57 AttributeScopes.AccessibilityAttributes.QueueAnnouncementAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
58 AttributeScopes.AccessibilityAttributes.IPANotationAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
59 AttributeScopes.AccessibilityAttributes.IPANotationAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
60 AttributeScopes.AccessibilityAttributes.AnnouncementPriorityAttribute AttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
61 AttributeScopes.AccessibilityAttributes.AnnouncementPriorityAttribute MarkdownDecodableAttributedStringKey YES false 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
62 AttributeScopes.FoundationAttributes AttributedStringKey no true 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
63 AttributeScopes.FoundationAttributes AttributeScope YES true 1 static AttributeScope.scopeDescription.getter (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_:including:) (Foundation)
64 AttributeScopes.FoundationAttributes.LinkAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
65 AttributeScopes.FoundationAttributes.LinkAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
66 AttributeScopes.FoundationAttributes.LanguageIdentifierAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
67 AttributeScopes.FoundationAttributes.LanguageIdentifierAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
68 AttributeScopes.FoundationAttributes.PersonNameComponentAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
69 AttributeScopes.FoundationAttributes.PersonNameComponentAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
70 AttributeScopes.FoundationAttributes.NumberFormatAttributes AttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
71 AttributeScopes.FoundationAttributes.NumberFormatAttributes AttributeScope YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
72 AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute AttributedStringKey YES true 3 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
73 AttributeScopes.FoundationAttributes.NumberFormatAttributes.SymbolAttribute MarkdownDecodableAttributedStringKey no true 3 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
74 AttributeScopes.FoundationAttributes.NumberFormatAttributes.NumberPartAttribute AttributedStringKey YES true 3 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
75 AttributeScopes.FoundationAttributes.NumberFormatAttributes.NumberPartAttribute MarkdownDecodableAttributedStringKey no true 3 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
76 AttributeScopes.FoundationAttributes.DateFieldAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
77 AttributeScopes.FoundationAttributes.DateFieldAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
78 AttributeScopes.FoundationAttributes.AlternateDescriptionAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
79 AttributeScopes.FoundationAttributes.AlternateDescriptionAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
80 AttributeScopes.FoundationAttributes.ImageURLAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
81 AttributeScopes.FoundationAttributes.ImageURLAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
82 AttributeScopes.FoundationAttributes.ReplacementIndexAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
83 AttributeScopes.FoundationAttributes.ReplacementIndexAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
84 AttributeScopes.FoundationAttributes.MeasurementAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
85 AttributeScopes.FoundationAttributes.MeasurementAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
86 AttributeScopes.FoundationAttributes.ByteCountAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
87 AttributeScopes.FoundationAttributes.ByteCountAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
88 AttributeScopes.FoundationAttributes.DurationFieldAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
89 AttributeScopes.FoundationAttributes.DurationFieldAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
90 AttributeScopes.FoundationAttributes.AgreementConceptAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
91 AttributeScopes.FoundationAttributes.AgreementConceptAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
92 AttributeScopes.FoundationAttributes.AgreementArgumentAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
93 AttributeScopes.FoundationAttributes.AgreementArgumentAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
94 AttributeScopes.FoundationAttributes.ReferentConceptAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
95 AttributeScopes.FoundationAttributes.ReferentConceptAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
96 AttributeScopes.FoundationAttributes.LocalizedNumberFormatAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
97 AttributeScopes.FoundationAttributes.LocalizedNumberFormatAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
98 AttributeScopes.FoundationAttributes.InlinePresentationIntentAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
99 AttributeScopes.FoundationAttributes.InlinePresentationIntentAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
100 AttributeScopes.FoundationAttributes.PresentationIntentAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
101 AttributeScopes.FoundationAttributes.PresentationIntentAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
102 AttributeScopes.FoundationAttributes.MarkdownSourcePositionAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
103 AttributeScopes.FoundationAttributes.MarkdownSourcePositionAttribute MarkdownDecodableAttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
104 AttributeScopes.FoundationAttributes.LocalizedStringArgumentAttributes AttributedStringKey no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
105 AttributeScopes.FoundationAttributes.LocalizedStringArgumentAttributes AttributeScope no true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
106 AttributeScopes.FoundationAttributes.InflectionAlternativeAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
107 AttributeScopes.FoundationAttributes.InflectionAlternativeAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
108 AttributeScopes.FoundationAttributes.MorphologyAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
109 AttributeScopes.FoundationAttributes.MorphologyAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
110 AttributeScopes.FoundationAttributes.InflectionRuleAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
111 AttributeScopes.FoundationAttributes.InflectionRuleAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
112 AttributeScopes.FoundationAttributes.AssumedFallbackInflectionAttribute AttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
113 AttributeScopes.FoundationAttributes.AssumedFallbackInflectionAttribute MarkdownDecodableAttributedStringKey YES true 2 static AttributeScope.scopeDescription.getter (Foundation) ← static AttributeScope.scopeDescription.getter (Foundation)
114 AttributeScopes.UIKitAttributes.FontAttribute ObjectiveCConvertibleAttributedStringKey no false 0 specialized Dictionary<>.init(\*\\\_:attributeTable:options:) (Foundation) ← NSAttributedString.init<Ο„\\\_0_0>(\\\_\*:including:) (Foundation)

By suggested optimizations we can get rid almost half of them: 52 of 114. So depending on approximation of total casting overhead inside AttributeContainer.init:

  1. If we suppose that total casting overhead is 90% of AttributeContainer.init - we will get 52 / 114 * 0.9 = ~40% boost
  2. if 50% - then 52 / 114 * 0.5 = ~ 22% boost.

That's how I've got my 20-40% approximation.

I'd be curious to better understand how much of a performance benefit this gives us

Lets come up with some basic approximation for p10 and p50 (median).

Percentile Today βˆ’20% βˆ’40%
p10 44 ms 35 ms (βˆ’9 ms) 26 ms (βˆ’18 ms)
p50 66 ms 53 ms (βˆ’13 ms) 40 ms (βˆ’26 ms)

It may seem insignificant, but I suppose that large portion of all iOS apps is using modern AttributeContainer API and face this slowdown somewhere at app startup / navigation.

On the other hand, our app is enourmously big and we have 205k protocol conformance descriptors. Average enterprise app would have about 80-100k. So we need to downscale our boost by 30% (why not 50%? Because 205k protocol conformance descriptors are introduced by our statically linked binary, and when each dynamic library is loaded, its set of protocol conformance descriptors is appended to the existing list in Swift Runtime, lets say all system dynamic libraries hold about 100k protocol conformance descriptors). So new boost approximation for average enterprise app would be:

Percentile Today βˆ’20% βˆ’40%
p10 31 ms 25 ms (βˆ’6 ms) 18 ms (βˆ’13 ms)
p50 46 ms 37 ms (βˆ’9 ms) 28 ms (βˆ’18 ms)

In my opinion, even with such insignificant numbers optimization is worth it because of scale: it is 13-18 ms boost on main thread in almost every app.

P.S. Based on our app measurements in production environment, p50 (median) for successful swift_conformsToProtocolMaybeInstantiateSuperclasses (first call for given type and protocol) is about 1.3-1.5 ms (it depends on where specific protocol conformance descriptor is
located in binary). For unsuccessful casts - even more, 2.4-2.8 ms (because we are doomed to scan all protocol conformance descriptors, rather than stopping at first matching one). These measurements are correct only when single cast is performed. If multiple casts are performed consequently (this case), overhead drops significantly, due to cache locality. That’s why p50 for AttributeContainer.init is significantly less than 1.3 ms * 112. And single cast inside AttributeContainer.init is only about 500 microseconds