Skip to content

Commit 1e9a609

Browse files
authored
Reduce size of cachedFixedComponentsLocales (#1328)
* Reduce size of `cachedFixedComponentsLocales` Previously we maintain a cache `private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol]`, and every call to `Locale(components: Locale.Components)` reads and writes to the cache. This results in a cache with non trivial size of keys. Optimize this by replacing the key with a String instead of the full `Locale.Components` instance. Using this as the baseline: ```swift for identifier in Locale.availableIdentifiers { let locale = Locale(identifier: identifier) } ``` With the following as the testing code: ```swift for identifier in Locale.availableIdentifiers { let components = Locale.Components(identifier: identifier) let locale = Locale(components: components) let components2 = Locale.Components(locale: locale) let locale2 = Locale(components: components2) // cache hit } ``` Previously, this incurred +2.6 MB of footprint comparing to the above baseline. After the fix, it incurred only +0.9MB increase. Also benchmarked that we have not regressed the time now that we are using the string-based identifier as the key. Fixes 144642686 * Add a comment about using Locale.Components.icuIdentifier as the cache key
1 parent 9721b41 commit 1e9a609

File tree

4 files changed

+66
-38
lines changed

4 files changed

+66
-38
lines changed

Benchmarks/Benchmarks/Internationalization/BenchmarkLocale.swift

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@
1313
import Benchmark
1414
import func Benchmark.blackHole
1515

16-
#if FOUNDATION_FRAMEWORK // This test uses CFString
16+
#if os(macOS) && USE_PACKAGE
17+
import FoundationEssentials
18+
import FoundationInternationalization
19+
#else
1720
import Foundation
21+
#endif
1822

1923
let benchmarks = {
2024
Benchmark.defaultConfiguration.maxIterations = 1_000
2125
Benchmark.defaultConfiguration.maxDuration = .seconds(3)
2226
Benchmark.defaultConfiguration.scalingFactor = .kilo
23-
Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .mallocCountTotal, .throughput]
27+
Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .throughput, .peakMemoryResident, .peakMemoryResidentDelta]
2428

29+
#if FOUNDATION_FRAMEWORK
2530
let string1 = "aaA" as CFString
2631
let string2 = "AAà" as CFString
2732
let range1 = CFRange(location: 0, length: CFStringGetLength(string1))
@@ -34,5 +39,22 @@ let benchmarks = {
3439
CFStringCompareWithOptionsAndLocale(string1, string2, range1, .init(rawValue: 0), nsLocale)
3540
}
3641
}
37-
}
3842
#endif
43+
44+
let identifiers = Locale.availableIdentifiers
45+
let allComponents = identifiers.map { Locale.Components(identifier: $0) }
46+
Benchmark("LocaleInitFromComponents") { benchmark in
47+
for components in allComponents {
48+
let locale = Locale(components: components)
49+
let components2 = Locale.Components(locale: locale)
50+
let locale2 = Locale(components: components2) // cache hit
51+
}
52+
}
53+
54+
Benchmark("LocaleComponentsInitIdentifer") { benchmark in
55+
for identifier in identifiers {
56+
let components = Locale.Components(identifier: identifier)
57+
}
58+
}
59+
}
60+

Sources/FoundationEssentials/Locale/Locale+Components.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,42 @@ extension Locale {
8585
public init(languageCode: Locale.LanguageCode? = nil, script: Locale.Script? = nil, languageRegion: Locale.Region? = nil) {
8686
self.languageComponents = Language.Components(languageCode: languageCode, script: script, region: languageRegion)
8787
}
88+
89+
// Returns an ICU-style identifier like "de_DE@calendar=gregorian"
90+
// Must include every component stored by a `Locale.Components`, and be kept in sync with `init(identifier:)`.
91+
package var icuIdentifier: String {
92+
93+
var keywords = [(ICULegacyKey, String)]()
94+
if let id = calendar?.cldrIdentifier { keywords.append((Calendar.Identifier.legacyKeywordKey, id)) }
95+
if let id = collation?._normalizedIdentifier { keywords.append((Locale.Collation.legacyKeywordKey, id)) }
96+
if let id = currency?._normalizedIdentifier { keywords.append((Locale.Currency.legacyKeywordKey, id)) }
97+
if let id = numberingSystem?._normalizedIdentifier { keywords.append((Locale.NumberingSystem.legacyKeywordKey, id)) }
98+
if let id = firstDayOfWeek?.rawValue { keywords.append((Locale.Weekday.legacyKeywordKey, id)) }
99+
if let id = hourCycle?.rawValue { keywords.append((Locale.HourCycle.legacyKeywordKey, id)) }
100+
if let id = measurementSystem?._normalizedIdentifier { keywords.append((Locale.MeasurementSystem.legacyKeywordKey, id)) }
101+
// No need for redundant region keyword
102+
if let region = region, region != languageComponents.region {
103+
// rg keyword value is actually a subdivision code
104+
keywords.append((Locale.Region.legacyKeywordKey, Locale.Subdivision.subdivision(for: region)._normalizedIdentifier))
105+
}
106+
if let id = subdivision?._normalizedIdentifier { keywords.append((Locale.Subdivision.legacyKeywordKey, id)) }
107+
if let id = timeZone?.identifier { keywords.append((TimeZone.legacyKeywordKey, id)) }
108+
if let id = variant?._normalizedIdentifier { keywords.append((Locale.Variant.legacyKeywordKey, id)) }
109+
110+
var locID = languageComponents.identifier
111+
let keywordCounts = keywords.count
112+
if keywordCounts > 0 {
113+
locID.append("@")
114+
}
115+
116+
for (i, (key, val)) in keywords.enumerated() {
117+
locID.append("\(key.key)=\(val)")
118+
if i != keywordCounts - 1 {
119+
locID.append(";")
120+
}
121+
}
122+
return locID
123+
}
88124
}
89125
}
90126

Sources/FoundationEssentials/Locale/Locale_Cache.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ struct LocaleCache : Sendable, ~Copyable {
4545
}
4646

4747
private var cachedFixedLocales: [String : any _LocaleProtocol] = [:]
48-
private var cachedFixedComponentsLocales: [Locale.Components : any _LocaleProtocol] = [:]
48+
private var cachedFixedComponentsLocales: [String /*ICU identifier*/: any _LocaleProtocol] = [:]
4949

5050
#if FOUNDATION_FRAMEWORK
5151
private var cachedFixedIdentifierToNSLocales: [String : _NSSwiftLocale] = [:]
@@ -99,17 +99,14 @@ struct LocaleCache : Sendable, ~Copyable {
9999

100100
#endif // FOUNDATION_FRAMEWORK
101101

102-
func fixedComponents(_ comps: Locale.Components) -> (any _LocaleProtocol)? {
103-
cachedFixedComponentsLocales[comps]
104-
}
105-
106102
mutating func fixedComponentsWithCache(_ comps: Locale.Components) -> any _LocaleProtocol {
107-
if let l = fixedComponents(comps) {
103+
let identifier = comps.icuIdentifier
104+
if let l = cachedFixedComponentsLocales[identifier] {
108105
return l
109106
} else {
110107
let new = _localeICUClass().init(components: comps)
111108

112-
cachedFixedComponentsLocales[comps] = new
109+
cachedFixedComponentsLocales[identifier] = new
113110
return new
114111
}
115112
}

Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,7 @@ internal import _FoundationICU
2222

2323
@available(macOS 13, iOS 16, tvOS 16, watchOS 9, *)
2424
extension Locale.Components {
25-
// Returns an ICU-style identifier like "de_DE@calendar=gregorian"
26-
internal var icuIdentifier: String {
27-
var keywords: [ICULegacyKey: String] = [:]
28-
if let id = calendar?.cldrIdentifier { keywords[Calendar.Identifier.legacyKeywordKey] = id }
29-
if let id = collation?._normalizedIdentifier { keywords[Locale.Collation.legacyKeywordKey] = id }
30-
if let id = currency?._normalizedIdentifier { keywords[Locale.Currency.legacyKeywordKey] = id }
31-
if let id = numberingSystem?._normalizedIdentifier { keywords[Locale.NumberingSystem.legacyKeywordKey] = id }
32-
if let id = firstDayOfWeek?.rawValue { keywords[Locale.Weekday.legacyKeywordKey] = id }
33-
if let id = hourCycle?.rawValue { keywords[Locale.HourCycle.legacyKeywordKey] = id }
34-
if let id = measurementSystem?._normalizedIdentifier { keywords[Locale.MeasurementSystem.legacyKeywordKey] = id }
35-
// No need for redundant region keyword
36-
if let region = region, region != languageComponents.region {
37-
// rg keyword value is actually a subdivision code
38-
keywords[Locale.Region.legacyKeywordKey] = Locale.Subdivision.subdivision(for: region)._normalizedIdentifier
39-
}
40-
if let id = subdivision?._normalizedIdentifier { keywords[Locale.Subdivision.legacyKeywordKey] = id }
41-
if let id = timeZone?.identifier { keywords[TimeZone.legacyKeywordKey] = id }
42-
if let id = variant?._normalizedIdentifier { keywords[Locale.Variant.legacyKeywordKey] = id }
43-
44-
var locID = languageComponents.identifier
45-
for (key, val) in keywords {
46-
// This uses legacy key-value pairs, like "collation=phonebook" instead of "-cu-phonebk", so be sure that the above values are `legacyKeywordKey`
47-
// See Locale.Components.legacyKey(forKey:) for more info on performance costs
48-
locID = Locale.identifierWithKeywordValue(locID, key: key, value: val)
49-
}
50-
return locID
51-
}
52-
25+
5326
/// - Parameter identifier: Unicode language identifier such as "en-u-nu-thai-ca-buddhist-kk-true"
5427
public init(identifier: String) {
5528
let languageComponents = Locale.Language.Components(identifier: identifier)

0 commit comments

Comments
 (0)