【问题标题】:Limit the results of a Swift array filter to X for performance将 Swift 数组过滤器的结果限制为 X 以提高性能
【发布时间】:2017-01-26 10:51:32
【问题描述】:

我的数组中有大约 2000 个元素,当它被过滤时,我想在过滤后的数组中有 5 个元素后立即结束过滤。

目前是:

providerArray.filter({($0.lowercased().range(of:((row.value as? String)?.lowercased())!) != nil)})

最多可以返回 2000 个结果,这既浪费处理时间,又浪费时间。

为了更清楚,我需要一个相当于限制过滤结果的解决方案,就像我可以使用 coreData fetches [request setFetchLimit:5];

【问题讨论】:

    标签: ios swift nspredicate


    【解决方案1】:

    就执行时间而言,最快的解决方案似乎是 显式循环添加匹配元素直到达到限制:

    extension Sequence {
        public func filter(where isIncluded: (Iterator.Element) -> Bool, limit: Int) -> [Iterator.Element] {
            var result : [Iterator.Element] = []
            result.reserveCapacity(limit)
            var count = 0
            var it = makeIterator()
    
            // While limit not reached and there are more elements ...
            while count < limit, let element = it.next() {
                if isIncluded(element) {
                    result.append(element)
                    count += 1
                }
            }
            return result
        }
    }
    

    示例用法:

    let numbers = Array(0 ..< 2000)
    let result = numbers.filter(where: { $0 % 3 == 0 }, limit: 5)
    print(result) // [0, 3, 6, 9, 12]
    

    【讨论】:

    • 又一次向显式循环致敬,整洁! :) 一个极端情况,但您可能需要考虑保留一个最小容量为limit 和序列的underestimatedCount(例如result.reserveCapacity(Swift.min(limit, underestimatedCount))),以防用户(误用)以巨大的限制调用它在一个小得多的序列上。
    • @dfri:我不确定这是否更好。对于长度未知的所有序列,underestimatedCount 为零,因此这仅适用于数组或其他 RandomAccessCollections。
    • 啊,是的,当然。那么也许只有在扩展名到Collection而不是Sequence的情况下:)有什么特别的理由在这里使用Sequence而不是Collection的更通用的扩展名吗? (因为我们返回一个更具体的类型为Collection 而不仅仅是Sequence)。忍者编辑:但是std lib filter of Sequence 也是如此......
    【解决方案2】:

    您也可以使用.lazy 来提高性能:

    let numbers: [Int] = Array(0 ..< 2000)
    
    let result: AnySequence = numbers
        .lazy
        .filter {
            print("Calling filter for: \($0)")
            return ($0 % 3) == 0
        }
        .prefix(5)
    
    print(Array(result))
    

    这将仅为前 15 个值调用 filter 函数(直到找到通过过滤器的 5 个值)。

    现在您可以专注于提高filter 本身的性能。例如。通过缓存值。您不必这样做,但如果某些值不断重复,则可以大大提高性能。

    let numbers: [Int] = Array(0 ..< 2000)
    var filterCache: [Int: Bool] = [:]
    
    let result: AnySequence = numbers
        .lazy
        .filter {
            if let cachedResult = filterCache[$0] {
                return cachedResult
            }
    
            print("Calling filter for: \($0)")
            let result = (($0 % 3) == 0)
    
            filterCache[$0] = result
    
            return result
        }
        .prefix(5)
    
    print(Array(result))
    

    您可以将此方法直接应用于您的函数。

    另请注意,为了提高性能,您应该:

    • ((row.value as? String)?.lowercased())!保存到一个局部变量中,因为它被执行了多次

    • 使用选项简化表达式:

     let result: AnySequence = providerArray
         .lazy
         .filter {
           $0.range(of: row.value as! String, options: [.caseInsensitive]) != nil
         }
         .prefix(5)
    

    【讨论】:

    • 这会是一个有效的过滤器吗?我的意思是它只会对前 15 个值应用过滤器。
    • 如果您将result 注释为AnySequence,则filter 的谓词每个元素只会被调用一次(然后您可以取消缓存)。这是因为prefix(_:) 默认情况下它输出一个切片,首先需要对惰性集合进行索引,该集合必须通过过滤器,然后再次用于Array(_:) 初始化程序。虽然为什么要第三次运行它,但我不太确定。
    • 即使print(results.first!) 在打印第一个元素之前也会遍历索引 0...15, 0...3。 @Hamish 显然有解释,但行为可能出乎意料,而且解决方案并不明显(至少对我来说不是)。 – 顺便说一句,let result = numbers.makeIterator().lazy.filter ... 也可以。
    • @MartinR 是的,那是因为prefix(5)(在输出切片的情况下)需要将索引推进到过滤结果中的第 5 个元素,从而通过索引 0...15 . first 然后必须遍历 0...3 才能从过滤结果中找到第一个 :) 我计划在今天晚些时候对此进行更多研究,并可能会发布一个 Q&A 详细解释它,因为我同意这种行为是一点都不明显。
    • 我继续 posted a Q&A 解释了这种行为。
    猜你喜欢
    • 1970-01-01
    • 2014-05-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-18
    • 2016-06-06
    • 1970-01-01
    相关资源
    最近更新 更多