簡體   English   中英

Swift 數組 firstIndex 在匹配 2 個小 arrays 結構時需要 20 秒

[英]Swift Array firstIndex takes 20 seconds when matching 2 small arrays of structures

我正在使用swift / swiftui將一個數組的結構分配給另一個結構數組的屬性。 arrays 相當小。 figureArray大約有 4000 條記錄, specificsArray大約有 200 條記錄。 查找匹配firstIndex大約需要 20 秒。 即使我注釋掉對figureArrayspecifics分配,該過程也需要 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 計算並在恆定時間內查找特定內容(理想情況下至少是這樣)。這是以運行特定內容一次以創建也是線性的字典為代價的。

另一個好處是, SpecificsFigure都不需要是 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM