Skip to content

Commit 9fef06e

Browse files
authored
Merge pull request #12752 from hamishknight/dictionary-subscript-addressor
[stdlib] Use addressor for Dictionary's subscript(_:default:)
2 parents 1c771fb + 7876391 commit 9fef06e

File tree

6 files changed

+242
-31
lines changed

6 files changed

+242
-31
lines changed

benchmark/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ set(SWIFT_BENCH_MODULES
5757
single-source/DictionaryGroup
5858
single-source/DictionaryLiteral
5959
single-source/DictionaryRemove
60+
single-source/DictionarySubscriptDefault
6061
single-source/DictionarySwap
6162
single-source/DropFirst
6263
single-source/DropLast
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
//===--- DictionarySubscriptDefault.swift ---------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import TestsUtils
14+
15+
public let DictionarySubscriptDefault = [
16+
BenchmarkInfo(name: "DictionarySubscriptDefaultMutation",
17+
runFunction: run_DictionarySubscriptDefaultMutation,
18+
tags: [.validation, .api, .Dictionary]),
19+
BenchmarkInfo(name: "DictionarySubscriptDefaultMutationArray",
20+
runFunction: run_DictionarySubscriptDefaultMutationArray,
21+
tags: [.validation, .api, .Dictionary]),
22+
BenchmarkInfo(name: "DictionarySubscriptDefaultMutationOfObjects",
23+
runFunction: run_DictionarySubscriptDefaultMutationOfObjects,
24+
tags: [.validation, .api, .Dictionary]),
25+
BenchmarkInfo(name: "DictionarySubscriptDefaultMutationArrayOfObjects",
26+
runFunction:
27+
run_DictionarySubscriptDefaultMutationArrayOfObjects,
28+
tags: [.validation, .api, .Dictionary]),
29+
]
30+
31+
let count = 10_000
32+
let result = count / 100
33+
34+
@inline(never)
35+
public func run_DictionarySubscriptDefaultMutation(_ N: Int) {
36+
for _ in 1...N {
37+
38+
var dict = [Int: Int]()
39+
40+
for i in 0..<count {
41+
dict[i % 100, default: 0] += 1
42+
}
43+
44+
CheckResults(dict.count == 100)
45+
CheckResults(dict[0]! == result)
46+
}
47+
}
48+
49+
@inline(never)
50+
public func run_DictionarySubscriptDefaultMutationArray(_ N: Int) {
51+
for _ in 1...N {
52+
53+
var dict = [Int: [Int]]()
54+
55+
for i in 0..<count {
56+
dict[i % 100, default: []].append(i)
57+
}
58+
59+
CheckResults(dict.count == 100)
60+
CheckResults(dict[0]!.count == result)
61+
}
62+
}
63+
64+
// Hack to workaround the fact that if we attempt to increment the Box's value
65+
// from the subscript, the compiler will just call the subscript's getter (and
66+
// therefore not insert the instance) as it's dealing with a reference type.
67+
// By using a mutating method in a protocol extension, the compiler is forced to
68+
// treat this an actual mutation, so cannot call the getter.
69+
protocol P {
70+
associatedtype T
71+
var value: T { get set }
72+
}
73+
74+
extension P {
75+
mutating func mutateValue(_ mutations: (inout T) -> Void) {
76+
mutations(&value)
77+
}
78+
}
79+
80+
class Box<T : Hashable> : Hashable, P {
81+
var value: T
82+
83+
init(_ v: T) {
84+
value = v
85+
}
86+
87+
var hashValue: Int {
88+
return value.hashValue
89+
}
90+
91+
static func ==(lhs: Box, rhs: Box) -> Bool {
92+
return lhs.value == rhs.value
93+
}
94+
}
95+
96+
@inline(never)
97+
public func run_DictionarySubscriptDefaultMutationOfObjects(_ N: Int) {
98+
for _ in 1...N {
99+
100+
var dict = [Box<Int>: Box<Int>]()
101+
102+
for i in 0..<count {
103+
dict[Box(i % 100), default: Box(0)].mutateValue { $0 += 1 }
104+
}
105+
106+
CheckResults(dict.count == 100)
107+
CheckResults(dict[Box(0)]!.value == result)
108+
}
109+
}
110+
111+
@inline(never)
112+
public func run_DictionarySubscriptDefaultMutationArrayOfObjects(_ N: Int) {
113+
for _ in 1...N {
114+
115+
var dict = [Box<Int>: [Box<Int>]]()
116+
117+
for i in 0..<count {
118+
dict[Box(i % 100), default: []].append(Box(i))
119+
}
120+
121+
CheckResults(dict.count == 100)
122+
CheckResults(dict[Box(0)]!.count == result)
123+
}
124+
}

benchmark/utils/main.swift

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import DictionaryBridge
4444
import DictionaryGroup
4545
import DictionaryLiteral
4646
import DictionaryRemove
47+
import DictionarySubscriptDefault
4748
import DictionarySwap
4849
import DropFirst
4950
import DropLast
@@ -164,6 +165,7 @@ registerBenchmark(DictionaryBridge)
164165
registerBenchmark(DictionaryGroup)
165166
registerBenchmark(DictionaryLiteral)
166167
registerBenchmark(DictionaryRemove)
168+
registerBenchmark(DictionarySubscriptDefault)
167169
registerBenchmark(DictionarySwap)
168170
registerBenchmark(DropFirst)
169171
registerBenchmark(DropLast)

stdlib/public/core/HashedCollections.swift.gyb

+56-31
Original file line numberDiff line numberDiff line change
@@ -2153,9 +2153,11 @@ extension Dictionary {
21532153
get {
21542154
return _variantBuffer.maybeGet(key) ?? defaultValue()
21552155
}
2156-
set(newValue) {
2157-
// FIXME(performance): this loads and discards the old value.
2158-
_variantBuffer.updateValue(newValue, forKey: key)
2156+
mutableAddressWithNativeOwner {
2157+
let (_, address) = _variantBuffer
2158+
.pointerToValue(forKey: key, insertingDefault: defaultValue)
2159+
return (address, Builtin.castToNativeObject(
2160+
_variantBuffer.asNative._storage))
21592161
}
21602162
}
21612163

@@ -4865,6 +4867,17 @@ internal enum _Variant${Self}Buffer<${TypeParametersDecl}> : _HashBuffer {
48654867
}
48664868
}
48674869

4870+
@_inlineable // FIXME(sil-serialize-all)
4871+
@_versioned // FIXME(sil-serialize-all)
4872+
internal mutating func ensureNativeBuffer() {
4873+
#if _runtime(_ObjC)
4874+
if _fastPath(guaranteedNative) { return }
4875+
if case .cocoa(let cocoaBuffer) = self {
4876+
migrateDataToNativeBuffer(cocoaBuffer)
4877+
}
4878+
#endif
4879+
}
4880+
48684881
#if _runtime(_ObjC)
48694882
@_inlineable // FIXME(sil-serialize-all)
48704883
@_versioned // FIXME(sil-serialize-all)
@@ -5314,6 +5327,42 @@ internal enum _Variant${Self}Buffer<${TypeParametersDecl}> : _HashBuffer {
53145327
#endif
53155328
}
53165329
}
5330+
5331+
@_inlineable // FIXME(sil-serialize-all)
5332+
@_versioned // FIXME(sil-serialize-all)
5333+
internal mutating func nativePointerToValue(
5334+
forKey key: Key, insertingDefault defaultValue: () -> Value
5335+
) -> (inserted: Bool, pointer: UnsafeMutablePointer<Value>) {
5336+
5337+
var (i, found) = asNative._find(key, startBucket: asNative._bucket(key))
5338+
if found {
5339+
let pointer = nativePointerToValue(at: ._native(i))
5340+
return (inserted: false, pointer: pointer)
5341+
}
5342+
5343+
let minCapacity = NativeBuffer.minimumCapacity(
5344+
minimumCount: asNative.count + 1,
5345+
maxLoadFactorInverse: _hashContainerDefaultMaxLoadFactorInverse)
5346+
5347+
let (_, capacityChanged) = ensureUniqueNativeBuffer(minCapacity)
5348+
5349+
if capacityChanged {
5350+
i = asNative._find(key, startBucket: asNative._bucket(key)).pos
5351+
}
5352+
5353+
asNative.initializeKey(key, value: defaultValue(), at: i.offset)
5354+
asNative.count += 1
5355+
return (inserted: true, pointer: asNative.values + i.offset)
5356+
}
5357+
5358+
@_inlineable // FIXME(sil-serialize-all)
5359+
@_versioned // FIXME(sil-serialize-all)
5360+
internal mutating func pointerToValue(
5361+
forKey key: Key, insertingDefault defaultValue: () -> Value
5362+
) -> (inserted: Bool, pointer: UnsafeMutablePointer<Value>) {
5363+
ensureNativeBuffer()
5364+
return nativePointerToValue(forKey: key, insertingDefault: defaultValue)
5365+
}
53175366
%end
53185367

53195368
@_inlineable // FIXME(sil-serialize-all)
@@ -5358,20 +5407,8 @@ internal enum _Variant${Self}Buffer<${TypeParametersDecl}> : _HashBuffer {
53585407
internal mutating func insert(
53595408
_ value: Value, forKey key: Key
53605409
) -> (inserted: Bool, memberAfterInsert: Value) {
5361-
5362-
if _fastPath(guaranteedNative) {
5363-
return nativeInsert(value, forKey: key)
5364-
}
5365-
5366-
switch self {
5367-
case .native:
5368-
return nativeInsert(value, forKey: key)
5369-
#if _runtime(_ObjC)
5370-
case .cocoa(let cocoaBuffer):
5371-
migrateDataToNativeBuffer(cocoaBuffer)
5372-
return nativeInsert(value, forKey: key)
5373-
#endif
5374-
}
5410+
ensureNativeBuffer()
5411+
return nativeInsert(value, forKey: key)
53755412
}
53765413

53775414
%if Self == 'Dictionary':
@@ -5475,20 +5512,8 @@ internal enum _Variant${Self}Buffer<${TypeParametersDecl}> : _HashBuffer {
54755512
_ keysAndValues: S,
54765513
uniquingKeysWith combine: (Value, Value) throws -> Value
54775514
) rethrows where S.Element == (Key, Value) {
5478-
if _fastPath(guaranteedNative) {
5479-
try nativeMerge(keysAndValues, uniquingKeysWith: combine)
5480-
return
5481-
}
5482-
5483-
switch self {
5484-
case .native:
5485-
try nativeMerge(keysAndValues, uniquingKeysWith: combine)
5486-
#if _runtime(_ObjC)
5487-
case .cocoa(let cocoaStorage):
5488-
migrateDataToNativeBuffer(cocoaStorage)
5489-
try nativeMerge(keysAndValues, uniquingKeysWith: combine)
5490-
#endif
5491-
}
5515+
ensureNativeBuffer()
5516+
try nativeMerge(keysAndValues, uniquingKeysWith: combine)
54925517
}
54935518

54945519
@_inlineable // FIXME(sil-serialize-all)

test/stdlib/Inputs/DictionaryKeyValueTypes.swift

+25
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@ func equalsUnordered<T : Comparable>(_ lhs: [T], _ rhs: [T]) -> Bool {
2525
return lhs.sorted().elementsEqual(rhs.sorted())
2626
}
2727

28+
// A COW wrapper type that holds an Int.
29+
struct TestValueCOWTy {
30+
31+
class Base {
32+
var value: Int
33+
init(_ value: Int) { self.value = value }
34+
}
35+
36+
private var base: Base
37+
init(_ value: Int = 0) { self.base = Base(value) }
38+
39+
var value: Int {
40+
get { return base.value }
41+
set {
42+
if !isKnownUniquelyReferenced(&base) {
43+
base = Base(newValue)
44+
} else {
45+
base.value = newValue
46+
}
47+
}
48+
}
49+
50+
var baseAddress: Int { return unsafeBitCast(base, to: Int.self) }
51+
}
52+
2853
var _keyCount = _stdlib_AtomicInt(0)
2954
var _keySerial = _stdlib_AtomicInt(0)
3055

validation-test/stdlib/Dictionary.swift

+34
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ func getCOWFastDictionary() -> Dictionary<Int, Int> {
127127
return d
128128
}
129129

130+
func getCOWFastDictionaryWithCOWValues() -> Dictionary<Int, TestValueCOWTy> {
131+
var d = Dictionary<Int, TestValueCOWTy>(minimumCapacity: 10)
132+
d[10] = TestValueCOWTy(1010)
133+
d[20] = TestValueCOWTy(1020)
134+
d[30] = TestValueCOWTy(1030)
135+
return d
136+
}
137+
130138
func getCOWSlowDictionary() -> Dictionary<TestKeyTy, TestValueTy> {
131139
var d = Dictionary<TestKeyTy, TestValueTy>(minimumCapacity: 10)
132140
d[TestKeyTy(10)] = TestValueTy(1010)
@@ -804,6 +812,32 @@ DictionaryTestSuite.test("COW.Fast.DefaultedSubscriptDoesNotReallocate") {
804812
}
805813
}
806814

815+
DictionaryTestSuite.test("COW.Fast.DefaultedSubscriptDoesNotCopyValue") {
816+
do {
817+
var d = getCOWFastDictionaryWithCOWValues()
818+
let identityValue30 = d[30]!.baseAddress
819+
820+
// Increment the value without having to reallocate the underlying Base
821+
// instance, as uniquely referenced.
822+
d[30, default: TestValueCOWTy()].value += 1
823+
assert(identityValue30 == d[30]!.baseAddress)
824+
assert(d[30]!.value == 1031)
825+
826+
let value40 = TestValueCOWTy()
827+
let identityValue40 = value40.baseAddress
828+
829+
// Increment the value, reallocating the underlying Base, as not uniquely
830+
// referenced.
831+
d[40, default: value40].value += 1
832+
assert(identityValue40 != d[40]!.baseAddress)
833+
assert(d[40]!.value == 1)
834+
835+
// Keep variables alive.
836+
_fixLifetime(d)
837+
_fixLifetime(value40)
838+
}
839+
}
840+
807841
DictionaryTestSuite.test("COW.Fast.IndexForKeyDoesNotReallocate") {
808842
var d = getCOWFastDictionary()
809843
var identity1 = d._rawIdentifier()

0 commit comments

Comments
 (0)