I am trying to build a data structure in Swift that maps an Integer to an array of objects (a dictionary with int as key and array as value). The objects are extremely small, and they simply wrap a UIColor and an Int. I have two implementations one that uses a Swift array as the Dictionary's value type, while the other uses a NSMutableArray as the value type. My objective-C code performs extremely fast, but my Swift code is running egregiously slow. Ideally, I would not like to use an NSMutableArray, and would like to keep it as a Swift array. Reason for this is I am writing an algorithm and performance matters, I have noticed some overhead with objC_msgSend. Can anyone help me optimize my Swift code? Am I doing something wrong or is this just a byproduct of swift treating array's as value types? If it is, I would like to understand why the value type performs so slow in this case, what my options are, and how can this scenario can scale going forward? Below I have posted a code snippet and the resulting benchmarks:
Swift Array Code:
let numColors = colorCount(filter: filter, colorInfoCount: colorInfo.count)
var colorCountsArray: [Int] = [Int]()
var countToColorMap: [Int:[CountedColor]] = [Int:[CountedColor]](minimumCapacity: capacity)
var topColors = [CountedColor]()
var startTime = CACurrentMediaTime()
for (color, colorCount) in colorInfo {
colorCountsArray.append(colorCount)
if countToColorMap[colorCount] != nil {
countToColorMap[colorCount]?.append(CountedColor(color: color, colorCount: colorCount))
} else {
countToColorMap[colorCount] = [CountedColor(color: color, colorCount: colorCount)]
}
}
var endTime = CACurrentMediaTime()
print("Time after mapping: \(endTime - startTime)")
Swift Performance:
Time after mapping: 45.0881789259997
NSMutableArray code:
let numColors = colorCount(filter: filter, colorInfoCount: colorInfo.count)
var colorCountsArray: [Int] = [Int]()
var countToColorMap: [Int:NSMutableArray] = [Int:NSMutableArray](minimumCapacity: capacity)
var topColors = [CountedColor]()
var startTime = CACurrentMediaTime()
for (color, colorCount) in colorInfo {
colorCountsArray.append(colorCount)
if countToColorMap[colorCount] != nil {
countToColorMap[colorCount]?.add(CountedColor(color: color, colorCount: colorCount))
} else {
countToColorMap[colorCount] = NSMutableArray(object: CountedColor(color: color, colorCount: colorCount))
}
}
var endTime = CACurrentMediaTime()
print("Time after mapping: \(endTime - startTime)")
NSMutableArray Performance:
Time after mapping: 0.367132211999888
The colorInfo object is a dictionary mapping UIColor objects to an Integer value representing a count. The code essentially reverse maps this, mapping an integer to a UIColor array (its an array because multiple Colors can have the same count). The colorInfo has 60,000 UIColor, Int key value pairs inside of it.
Copy on write is a tricky thing, and you need to think carefully about how many things are sharing a structure that you're trying to modify. The culprit is here.
countToColorMap[colorCount]?.append(CountedColor(color: color as! UIColor, colorCount: colorCount))
This is generating a temporary value that is modified and put back into the dictionary. Since two "things" are looking at the same underlying data structure (the dictionary, and append
), it forces a copy-on-write.
The secret to fixing this is to make sure that there's only one copy when you modify it. How? Take it out of the dictionary. Replace this:
if countToColorMap[colorCount] != nil {
countToColorMap[colorCount]?.append(CountedColor(color: color as! UIColor, colorCount: colorCount))
} else {
countToColorMap[colorCount] = [CountedColor(color: color as! UIColor, colorCount: colorCount)]
}
which has a runtime of:
Elapsed Time: 74.2517465990022
53217
with this:
var countForColor = countToColorMap.removeValue(forKey: colorCount) ?? []
countForColor.append(CountedColor(color: color as! UIColor, colorCount: colorCount))
countToColorMap[colorCount] = countForColor
which has a runtime of:
Elapsed Time: 0.370953808000195
53217
I had some work arounds until swift 4.2 came along
var countToColorMap = [Int: [CountedColor]]()
for (color, colorCount) in colorInfo {
countToColorMap[colorCount, default: [CountedColor]()].append(CountedColor(color: color as! UIColor, colorCount: colorCount))
}
it is fast and readable
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With