【问题标题】:Swift Array firstIndex takes 20 seconds when matching 2 small arrays of structures匹配 2 个小结构数组时,Swift Array firstIndex 需要 20 秒
【发布时间】:2021-07-25 05:01:09
【问题描述】:

我正在使用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)
    }
}

【问题讨论】:

    标签: arrays swift performance loops


    【解决方案1】:

    这里的问题是您的算法在二次时间中运行。对于一个数组中的每个元素,您都在线性搜索另一个数组。最坏的情况,这意味着第二个数组的每个元素都与第一个数组的每个元素进行比较。 (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 都不需要是可散列的。

    试一试,看看你是否获得了性能提升。

    【讨论】:

    • 谢谢!惊人的快 - 大约 0.3 秒。 Start 2021-05-02 23:50:30.0830 Finish 2021-05-02 23:50:30.1120。我也很欣赏额外的描述和作为一个功能。这将在我的代码中的其他地方帮助我。
    • Here's a basic tutorial about Big-O notation. 也可以搜索其他教程。了解 Big-O 很重要。在您的情况下,运行次数最多的是比较闭包,并且它运行的次数随着输入呈指数增长。 (4000 * 400 次!)我的代码中运行次数最多的行是地图的闭包,它线性增加(4000 次。)
    【解决方案2】:

    问题在于,您可能会在每次修改 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
        }
    }
    

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

    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
    }
    

    当你把它变成一个类时,你复制数组的成本就会降低很多。该结构非常大,因此复制它很昂贵。当您复制一个类数组时,您只需在每个类上添加一个额外的保留计数并复制一个指针。当结构很大时,这可能会快得多。 (但如果可能的话,最好避免所有的复制。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-31
      • 2012-06-01
      • 1970-01-01
      • 2017-01-06
      • 2019-04-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多