[英]Swift Array firstIndex takes 20 seconds when matching 2 small arrays of structures
我正在使用swift
/ swiftui
將一個數組的結構分配給另一個結構數組的屬性。 arrays 相當小。 figureArray
大約有 4000 條記錄, specificsArray
大約有 200 條記錄。 查找匹配firstIndex
大約需要 20 秒。 即使我注釋掉對figureArray
的specifics
分配,該過程也需要 20 秒,這表明for
/ forEach
中的 firstIndex 非常慢。
在我的 iPhone 8 上,該進程的 memory 約為 90M,CPU 達到 ~100%
問題是,我怎樣才能讓它更快? (快得多 - 即不到 2 秒)。 對我來說,這個過程似乎需要幾毫秒的數組大小。
specifics
對象是唯一的,從不重疊,因此可以並行進行設置。 我只是不確定如何。
specificsArray.forEach { specific in
// look for a figure
if let indexFigure = figureArray.firstIndex(where: {$0.figureGlobalUniqueId == specific.specificsFirebase.figureGlobalUniqueId}) {
figureArray[indexFigure].specifics = specific
}
}
我也試過以下。 時間幾乎相同,大約 20 秒
for indexSpecifics in 0 ..< specificsArray.count {
// look for a figure
if let indexFigure = figureArray.firstIndex(where: {$0.figureGlobalUniqueId == specificsArray[indexSpecifics].specificsFirebase.figureGlobalUniqueId}) {
figureArray[indexFigure].specifics = specificsArray[indexSpecifics]
}
}
具體結構
struct Specifics: Hashable, Codable, Identifiable {
var id: UUID
var specificsFirebase: SpecificsFirebase
var isSet = false
}
struct SpecificsFirebase: Hashable, Codable, CustomStringConvertible {
let seriesUniqueId: String
let figureGlobalUniqueId: String
var loose_haveCount: Int = 0
var loose_sellCount: Int = 0
var loose_wantCount: Int = 0
var new_haveCount: Int = 0
var new_orderCount: Int = 0
var new_orderText: String = ""
var new_sellCount: Int = 0
var new_wantCount: Int = 0
var notes: String = ""
var updateDate: String = ""
// print description
var description: String {
return ("SpecificsStruct: \(seriesUniqueId), \(figureGlobalUniqueId), \n LOOSE: Have \(loose_haveCount), sell \(loose_sellCount), want \(loose_wantCount) \n NEW: Have \(new_haveCount), sell \(new_sellCount), want \(new_wantCount), order \(new_orderCount) \(new_orderText) \n notes \(notes), update date \(updateDate)")
}
func saveSpecifics(userID: String) {
setFirebaseSpecifics(userID: userID)
}
func setFirebaseSpecifics(userID: String) {
let firebaseRef: DatabaseReference! = Database.database().reference()
let specificsPath = SpecificsFirebase.getSpecificsFirebaseRef(userID: userID, seriesUniqueId: SeriesUniqueIdEnum(rawValue: seriesUniqueId)!,
figureGlobalUniqueId: figureGlobalUniqueId)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = kDateFormatDatabase // firebase 2020-09-13T14:34:47.336
let updateDate = Date()
let updateDateString = dateFormatter.string(from: updateDate)
let firebaseSpecifics = [
"figureGlobalUniqueId": figureGlobalUniqueId,
"loose_haveCount": loose_haveCount,
"loose_sellCount": loose_sellCount,
"loose_wantCount": loose_wantCount,
"new_haveCount": new_haveCount,
"new_orderCount": new_orderCount,
"new_orderText": new_orderText,
"new_sellCount": new_sellCount,
"new_wantCount": new_wantCount,
"notes": notes,
"seriesUniqueId": seriesUniqueId,
"updateDate": updateDateString
] as [String: Any]
// #if DEBUG
// print("Setting firebase specifics for \(firebaseSpecifics)")
// #endif
firebaseRef.child(specificsPath).setValue(firebaseSpecifics)
}
}
圖結構
struct Figure: Hashable, Codable, Identifiable {
var id = UUID()
// var id: String { Figure_Unique_ID }
func hash(into hasher: inout Hasher) {
hasher.combine(figureUniqueId)
}
let figureGlobalUniqueId: String
let seriesUniqueId: SeriesUniqueIdEnum
let figureUniqueId: String
let sortOrder: Int
let debutYear: Int?
let phase: String
let wave: String
var figureNumber: String?
let sortGrouping: String
var uPC: String?
let figureName: String
let figurePackageName: String
// var tags = [String]()
var scene: String?
var findTerms: String
var excludeTerms: String?
var amazonASIN: String?
var amazonShortLink: String?
var walmartSKU: String?
var targetTCIN: String?
var targetDPCI: String?
var entertainmentEarthIN: String?
var retailDate: Date?
var retailPrice: Float?
var addedDate: Date
// calculated or set
let primaryFrontImageName: String
let primaryFrontImageNameNoExt: String
// generated retail links
var searchString: String
// calculated later
var amazonURL: URL?
var entertainmentEarthURL: URL?
var targetURL: URL?
var walmartURL: URL?
var eBayURL: URL?
var specifics: Specifics
init (seriesUniqueId: SeriesUniqueIdEnum,
figureUniqueId: String,
sortOrder: Int,
debutYear: Int?,
phase: String,
wave: String,
figureNumber: String?,
// sortGrouping: String,
// tags: String?,
uPC: String?,
figureName: String,
figurePackageName: String,
scene: String?,
findTerms: String,
excludeTerms: String?,
amazonASIN: String?,
amazonShortLink: String?,
walmartSKU: String?,
targetTCIN: String?,
targetDPCI: String?,
entertainmentEarthIN: String?,
retailDate: Date?,
retailPrice: Float?,
addedDate: Date) {
self.seriesUniqueId = seriesUniqueId
self.figureUniqueId = figureUniqueId
self.figureGlobalUniqueId = "\(seriesUniqueId.rawValue)_\(figureUniqueId)"
self.sortOrder = sortOrder
self.debutYear = debutYear
self.phase = phase
self.wave = wave
self.figureNumber = figureNumber
self.sortGrouping = phase // <---------- Uses Phase!
self.uPC = uPC
self.figureName = figureName
self.figurePackageName = figurePackageName
self.scene = scene
self.findTerms = findTerms
self.excludeTerms = excludeTerms
self.amazonASIN = amazonASIN
self.amazonShortLink = amazonShortLink
self.walmartSKU = walmartSKU
self.targetTCIN = targetTCIN
self.targetDPCI = targetDPCI
self.entertainmentEarthIN = entertainmentEarthIN
self.retailDate = retailDate
self.retailPrice = retailPrice
self.addedDate = addedDate
// split out the hash tags
// if let tags = tags {
// let words = tags.components(separatedBy: " ")
// for word in words{
// if word.hasPrefix("#"){
//// let hashtag = word.dropFirst()
// self.tags.append(String(word))
// }
// }
// }
// set the specifics to the default so that the pickers work. Pickers don't like optionals.
// DONT SET the isSet here as this is a default record
self.specifics = Specifics(id: UUID(), specificsFirebase: SpecificsFirebase(seriesUniqueId: seriesUniqueId.rawValue, figureGlobalUniqueId: figureGlobalUniqueId))
// built fields
self.primaryFrontImageName = "\(seriesUniqueId.rawValue)_\(figureUniqueId)\(kPrimaryFrontImageNameSuffix)\(kSmallSuffix).\(kImageJpgExt)"
self.primaryFrontImageNameNoExt = "\(seriesUniqueId.rawValue)_\(figureUniqueId)\(kPrimaryFrontImageNameSuffix)\(kSmallSuffix)"
// generated
self.searchString = "\(seriesUniqueId) \(figureUniqueId), \(phase) \(wave) \(figurePackageName)"
if let figureNumber = figureNumber {
self.searchString += " \(figureNumber)"
}
if let uPC = uPC {
self.searchString += " \(uPC)"
}
if let amazonASIN = amazonASIN {
self.searchString += " \(amazonASIN)"
}
if let targetTCIN = targetTCIN {
self.searchString += " \(targetTCIN)"
}
if let targetDPCI = targetDPCI {
self.searchString += " \(targetDPCI)"
}
if let entertainmentEarthIN = entertainmentEarthIN {
self.searchString += " \(entertainmentEarthIN)"
}
if let scene = scene {
self.searchString += " \(scene)"
}
if let debutYear = debutYear {
self.searchString += " \(debutYear)"
}
}
enum CodingKeys: String, CodingKey {
case figureUniqueId = "Figure_Unique_ID"
case seriesUniqueId = "Series_Unique_ID"
case sortOrder = "Sort_Order"
case debutYear = "Debut_Year"
case phase = "Phase"
case wave = "Wave"
case figureNumber = "Number"
// case sortGrouping = "Sort_Grouping"
// case tags = "Tags"
case uPC = "UPC"
case figureName = "Action_Figure"
case figurePackageName = "Action_Figure_Package_Name"
case scene = "Scene"
case findTerms = "Find_Terms"
case excludeTerms = "Exclude_Terms"
case amazonASIN = "Amazon_ASIN"
case amazonShortLink = "Amazon_Short_Link"
case walmartSKU = "WalmartSKU"
case targetTCIN = "Target_TCIN"
case targetDPCI = "Target_DPCI"
case entertainmentEarthIN = "EEIN"
case retailDate = "Retail_Date"
case retailPrice = "Retail_Price"
case addedDate = "Added_Date"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let seriesUniqueIdString = try values.decode(String.self, forKey: .seriesUniqueId)
let figureUniqueId = try values.decode(String.self, forKey: .figureUniqueId)
let sortOrder = try values.decode(Int.self, forKey: .sortOrder)
let debutYear = try values.decode(Int.self, forKey: .debutYear)
let phase = try values.decode(String.self, forKey: .phase)
let wave = try values.decode(String.self, forKey: .wave)
let figureNumber = try? values.decode(String.self, forKey: .figureNumber)
// let sortGrouping = try values.decode(String.self, forKey: .sortGrouping)
// let tags = try? values.decode(String.self, forKey: .tags)
let uPC = try? values.decode(String.self, forKey: .uPC)
let figureName = try values.decode(String.self, forKey: .figureName)
let figurePackageName = try values.decode(String.self, forKey: .figurePackageName)
let scene = try? values.decode(String.self, forKey: .scene)
let findTerms = try values.decode(String.self, forKey: .findTerms)
let excludeTerms = try? values.decode(String.self, forKey: .excludeTerms)
let amazonASIN = try? values.decode(String.self, forKey: .amazonASIN)
let amazonShortLink = try? values.decode(String.self, forKey: .amazonShortLink)
let walmartSKU = try? values.decode(String.self, forKey: .walmartSKU)
let targetTCIN = try? values.decode(String.self, forKey: .targetTCIN)
let targetDPCI = try? values.decode(String.self, forKey: .targetDPCI)
let entertainmentEarthIN = try? values.decode(String.self, forKey: .entertainmentEarthIN)
let retailDateString = try? values.decode(String.self, forKey: .retailDate)
let retailPrice = try? values.decode(Float.self, forKey: .retailPrice)
let addedDateString = try? values.decode(String.self, forKey: .addedDate)
// calculated
let seriesUniqueId = SeriesUniqueIdEnum(rawValue: seriesUniqueIdString)!
// date logic
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM/dd/yyyy" //Your date format
var retailDate: Date? = nil
if let retailDateString = retailDateString {
if let retailDateValid = dateFormatter.date(from: retailDateString) {
retailDate = retailDateValid
}
}
var addedDate = defaultAddedDate
if let addedDateString = addedDateString {
if let addedDateValid = dateFormatter.date(from: addedDateString) {
addedDate = addedDateValid
}
}
self.init(seriesUniqueId: seriesUniqueId,
figureUniqueId: figureUniqueId,
sortOrder: sortOrder,
debutYear: debutYear,
phase: phase,
wave: wave,
figureNumber: figureNumber,
// sortGrouping: phase,
// tags: tags,
uPC: uPC,
figureName: figureName,
figurePackageName: figurePackageName,
scene: scene,
findTerms: findTerms,
excludeTerms: excludeTerms,
amazonASIN: amazonASIN,
amazonShortLink: amazonShortLink,
walmartSKU: walmartSKU,
targetTCIN: targetTCIN,
targetDPCI: targetDPCI,
entertainmentEarthIN: entertainmentEarthIN,
retailDate: retailDate,
retailPrice: retailPrice,
addedDate: addedDate)
}
func encode( to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.seriesUniqueId, forKey: .seriesUniqueId)
try container.encode(self.figureUniqueId, forKey: .figureUniqueId)
try container.encode(self.sortOrder, forKey: .sortOrder)
try container.encode(self.debutYear, forKey: .debutYear)
try container.encode(self.phase, forKey: .phase)
try container.encode(self.wave, forKey: .wave)
try container.encode(self.figureNumber, forKey: .figureNumber)
// try container.encode(self.sortGrouping, forKey: .sortGrouping)
// try container.encode(self.tags, forKey: .tags)
try container.encode(self.uPC, forKey: .uPC)
try container.encode(self.figureName, forKey: .figureName)
try container.encode(self.figurePackageName, forKey: .figurePackageName)
try container.encode(self.scene, forKey: .scene)
try container.encode(self.findTerms, forKey: .findTerms)
try container.encode(self.excludeTerms, forKey: .excludeTerms)
try container.encode(self.amazonASIN, forKey: .amazonASIN)
try container.encode(self.amazonShortLink, forKey: .amazonShortLink)
try container.encode(self.walmartSKU, forKey: .walmartSKU)
try container.encode(self.targetTCIN, forKey: .targetTCIN)
try container.encode(self.targetDPCI, forKey: .targetDPCI)
try container.encode(self.entertainmentEarthIN, forKey: .entertainmentEarthIN)
try container.encode(self.retailDate, forKey: .retailDate)
try container.encode(self.retailPrice, forKey: .retailPrice)
try container.encode(self.addedDate, forKey: .addedDate)
}
}
這里的問題是您的算法在二次時間中運行。 對於一個數組中的每個元素,您都在線性搜索另一個數組。 最壞的情況,這意味着第二個數組的每個元素都與第一個數組的每個元素進行比較。 (x * y 比較!)
這應該有助於:
func example(specificsArray: [Specifics], figureArray: inout [Figure]) {
let specificsDict = Dictionary(uniqueKeysWithValues: specificsArray
.map { ($0.specificsFirebase.figureGlobalUniqueId, $0) })
for (index, figure) in figureArray.enumerated() {
if let specific = specificsDict[figure.figureGlobalUniqueId] {
figureArray[index].specifics = specific
}
}
}
以上將大 O 時間降至線性(假設 hash 良好)。代碼不再將每個具體數字與每個數字進行比較。 相反,它正在執行 hash 計算並在恆定時間內查找特定內容(理想情況下至少是這樣)。這是以運行特定內容一次以創建也是線性的字典為代價的。
另一個好處是, Specifics
和Figure
都不需要是 Hashable。
嘗試一下,看看您是否獲得了性能改進。
問題是您可能會在每次對其進行變異時制作figureArray
的完整副本。 Swift 值類型是“寫入時復制”,這意味着它們可以隨時被復制。 不過,這通常是可以避免的。 如果您打開優化(即為 Release 構建),這可能會工作得更好,但在調試模式下,它可能無法避免復制。
避免這種情況的一種方法是扭轉這種情況,並迭代figureArray
而不是specificsArray
。 我希望這更容易優化。 它還避免了多次搜索大數組。 這恰好觸及大數組的每個元素一次,而不是 specificsArray 的每個元素的一半元素:
for index in figureArray.indices {
let id = figureArray[index].figureGlobalUniqueId
if let specific = specific.first(where: { id == specific.specificsFirebase.figureGlobalUniqueId }) {
figureArray[index].specifics = specific
}
}
這應該有望避免任何復制,但如果沒有,您可以通過將其轉換為 map 來確保只有一個副本而不是多個副本:
figureArray = figureArray.map { figure in
guard let specific = specificsArray.first(where: { specific in
specific.specificsFirebase.figureGlobalUniqueId == figure.figureGlobalUniqueId })
else { return figure } // Return the original value if nothing has changed
// Otherwise update it
var newFigure = figure
figure.specifics = figure
return figure
}
當您將其設為 class 時,復制陣列的成本會降低很多。 該結構非常大,因此復制它很昂貴。 當您復制一個類數組時,您只需在每個類上添加一個額外的保留計數並復制一個指針。 當結構很大時,這可能會快得多。 (但如果可能的話,最好避免所有的復制。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.