Swift Array firstIndex在匹配2个小数组结构时需要20秒



我使用swift/swiftui将一个数组中的结构分配给另一个结构数组的属性。阵列相当小。figureArray是大约4000个记录,而specificsArray是大约200个记录。查找匹配firstIndex花费大约20秒。即使我注释掉specificsfigureArray的分配,该过程也需要20秒,这表明for/forEach中的firstIndex非常慢。

在我的iPhone 8上,该过程的内存约为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
}
}
}

以上将把big-O时间降为线性(假设哈希值良好(。代码不再将每个特定数字与每个数字进行比较。相反,它正在进行哈希计算,并在恒定时间内查找特定数字(理想情况下至少如此(。这是以运行一次特定数字来创建同样是线性的词典为代价的。

另一个好处是SpecificsFigure都不需要是Hashable。

试试看你的表现是否有所改善。

问题是,每次对figureArray进行变异时,都可能会生成它的完整副本;写时复制;这意味着它们可以在任何时候被复制。不过,这通常是可以避免的。如果启用优化(即为Release构建(,这可能会更好地工作,但在调试模式下,它可能无法避免复制。

避免这种情况的一种方法是扭转这种局面,并在figureArray上迭代,而不是在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
}
}

这应该有望避免任何复制,但如果没有,你可以通过将其转化为地图来确保只有一个副本,而不是多个:

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
}

当您将其作为一个类时,您使复制数组变得更加便宜。该结构非常大,因此复制它的成本很高。当您复制一个类数组时,您只需要在每个类上添加一个额外的保留计数并复制一个指针。当结构是巨大的时,速度会快得多。(但如果可能的话,最好避免所有的复制。(

最新更新