如何对Swift代码执行进行基准测试?

96

除了以下方法,是否有一种方法/软件可以给出编写在Swift中的代码块所需的精确时间?

let date_start = NSDate()

// Code to be executed 

println("\(-date_start.timeIntervalSinceNow)")

1
除了使用Instruments之外,您是指您实际想要为自己获取这些东西吗?您不仅仅想作为一个人知道它,而是需要您的代码能够解决它? - Tommy
9个回答

166

如果你只是想为一块代码使用独立的计时函数,我使用以下的Swift辅助函数:

func printTimeElapsedWhenRunningCode(title:String, operation:()->()) {
    let startTime = CFAbsoluteTimeGetCurrent()
    operation()
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print("Time elapsed for \(title): \(timeElapsed) s.")
}

func timeElapsedInSecondsWhenRunningCode(operation: ()->()) -> Double {
    let startTime = CFAbsoluteTimeGetCurrent()
    operation()
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    return Double(timeElapsed)
}

前者将记录给定代码部分所需的时间,后者将其作为浮点数返回。以下是第一种变体的示例:

printTimeElapsedWhenRunningCode(title:"map()") {
    let resultArray1 = randoms.map { pow(sin(CGFloat($0)), 10.0) }
}

将记录类似以下内容:

map() 执行的时间:0.0617449879646301 秒

请注意,Swift 基准测试的结果会严重受到所选优化级别的影响,因此这只对比较 Swift 执行时间有用。即使如此,在每个测试版中,结果可能也会发生变化。


4
谢谢您的回答,非常有帮助。 - ielyamani
使用这种技术的一个问题是它引入了一个新作用域,因此在该作用域内声明的任何内容都不会在其外部可用(例如,在您的示例中,resultArray1 将无法在闭包之后的代码中使用)。 - pbouf77

43
如果您想深入了解某个代码块的性能并确保在进行编辑时不会影响性能,最好使用XCTest的测量性能函数,例如measure(_ block: () -> Void)
编写一个单元测试来执行您想要基准测试的方法,该单元测试将多次运行它,给出所需的时间和结果偏差。
func testExample() {

    self.measure {
        //do something you want to measure
    }
}

您可以在苹果文档中找到更多信息,具体位置为使用Xcode进行测试 -> 性能测试


7
请注意,measureBlock 会多次执行代码块以获取更好的统计数据。 - Eneko Alonso
measureBlock是什么?它来自哪里(我应该导入什么)?在Swift 4中。 - user924
measureBlock()是XCTestCase中的一个方法。导入 XCTest... 文件 -> 新建 -> 测试用例 模板会默认添加该块。 - kviksilver

19
你可以使用这个函数来测量异步和同步的代码:
import Foundation

func measure(_ title: String, block: (@escaping () -> ()) -> ()) {

    let startTime = CFAbsoluteTimeGetCurrent()

    block {
        let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
        print("\(title):: Time: \(timeElapsed)")
    }
}

基本上你需要传递一个接受函数作为参数的块,用于告诉 measure 何时完成。

例如,要测量名为“myAsyncCall”的某个调用需要多长时间,您可以像这样调用它:

measure("some title") { finish in
    myAsyncCall {
        finish()
    }
    // ...
}

对于同步代码:

measure("some title") { finish in
     // code to benchmark
     finish()
     // ...
}

这应该与 XCTest 的 measureBlock 类似,虽然我不知道它在那里是如何实现的。


@escaping 是必需的。 - Cameron Lowell Palmer

13

基准测试功能 - Swift 4.2

这是一个非常通用的基准测试功能,允许对测试进行标记,执行多个测试并平均它们的执行时间,在测试之间调用设置块(即在测量数组排序算法之间洗牌数组),清晰地打印基准测试结果,并将平均执行时间作为Double返回。

尝试以下内容:

@_transparent @discardableResult public func measure(label: String? = nil, tests: Int = 1, printResults output: Bool = true, setup: @escaping () -> Void = { return }, _ block: @escaping () -> Void) -> Double {
    
    guard tests > 0 else { fatalError("Number of tests must be greater than 0") }
    
    var avgExecutionTime : CFAbsoluteTime = 0
    for _ in 1...tests {
        setup()
        let start = CFAbsoluteTimeGetCurrent()
        block()
        let end = CFAbsoluteTimeGetCurrent()
        avgExecutionTime += end - start
    }
    
    avgExecutionTime /= CFAbsoluteTime(tests)
    
    if output {
        let avgTimeStr = "\(avgExecutionTime)".replacingOccurrences(of: "e|E", with: " × 10^", options: .regularExpression, range: nil)
        
        if let label = label {
            print(label, "▿")
            print("\tExecution time: \(avgTimeStr)s")
            print("\tNumber of tests: \(tests)\n")
        } else {
            print("Execution time: \(avgTimeStr)s")
            print("Number of tests: \(tests)\n")
        }
    }
    
    return avgExecutionTime
}

用法

var arr = Array(1...1000).shuffled()

measure(label: "Map to String") {
    let _ = arr.map { String($0) }
}

measure(label: "Apple's Sorting Method", tests: 1000, setup: { arr.shuffle() }) {
    arr.sort()
}

measure {
    let _ = Int.random(in: 1...10000)
}

let mathExecutionTime = measure(printResults: false) {
    let _ = 219 * 354
}

print("Math Execution Time: \(mathExecutionTime * 1000)ms")


// Prints:
//
// Map to String ▿
//     Execution time: 0.021643996238708496s
//     Number of tests: 1
//
// Apple's Sorting Method ▿
//     Execution time: 0.0010601345300674438s
//     Number of tests: 1000
//
// Execution time: 6.198883056640625 × 10^-05s
// Number of tests: 1
//
// Math Execution Time: 0.016927719116210938ms
//

注意:measure函数还返回执行时间。参数labeltestssetup是可选的。参数printResults默认为true


3
我喜欢Brad Larson的答案,它提供了一个简单的测试方法,你甚至可以在Playground中运行它。针对我的需求,我稍微改进了一下:
  1. 我将要测试的函数调用放在一个测试函数中,这样我就能够灵活地尝试不同的参数。
  2. 测试函数返回其名称,所以我不必在benchmarking函数中包含“title”参数。
  3. benchmarking函数允许您可选地指定要执行的重复次数(默认为10)。它会报告总时间和平均时间。
  4. benchmarking函数既向控制台打印又返回averageTime(你可能会从名为“averageTimeTo”的函数中期望这个),因此没有必要使用两个几乎具有相同功能的单独函数。
例如:
func myFunction(args: Int...) {
    // Do something
}

func testMyFunction() -> String {
    // Wrap the call to myFunction here, and optionally test with different arguments
    myFunction(args: 1, 2, 3)
    return #function
}

// Measure average time to complete test
func averageTimeTo(_ testFunction: () -> String, repeated reps: UInt = 10) -> Double {
    let functionName = testFunction()
    var totalTime = 0.0
    for _ in 0..<reps {
        let startTime = CFAbsoluteTimeGetCurrent()
        testFunction()
        let elapsedTime = CFAbsoluteTimeGetCurrent() - startTime
        totalTime += elapsedTime
    }
    let averageTime = totalTime / Double(reps)
    print("Total time to \(functionName) \(reps) times: \(totalTime) seconds")
    print("Average time to \(functionName): \(averageTime) seconds\n")
    return averageTime
}

averageTimeTo(testMyFunction)

// Total time to testMyFunction() 10 times: 0.000253915786743164 seconds
// Average time to testMyFunction(): 2.53915786743164e-05 seconds

averageTimeTo(testMyFunction, repeated: 1000)

// Total time to testMyFunction() 1000 times: 0.027538537979126 seconds
// Average time to testMyFunction(): 2.7538537979126e-05 seconds

1
这很聪明!谢谢您分享。由于您已经打印了averageTime,因此@discardableResult可能会很有用。 - ielyamani
是的,说得好。如果你不习惯使用返回值(就像我的示例中所显示的那样!),那将更加迅速。我甚至没有注意到,在playgrounds中它并不会生成警告。 - Kal

3

对 @Brad Larson 的回答(被采纳的答案)进行小幅度改进,以允许函数如果需要返回操作结果。

@discardableResult func printTimeElapsedWhenRunningCode<T>(title: String, operation: () -> T) -> T {
    let startTime = CFAbsoluteTimeGetCurrent()
    let result = operation()
    let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
    print("Time elapsed for \(title): \(timeElapsed) s.")
    
    return result
}

3

基准测试Swift代码执行

请在Production构建上进行基准测试,而不是Debug,因为它包含更多的优化。

你有几种可能性

  • DispatchTime.now().uptimeNanoseconds (并除以1_000_000)

    基于机器绝对时间单位

壁钟(由于闰秒或其他微小修正,不推荐使用

  • Date().timeIntervalSince1970
  • CFAbsoluteTimeGetCurrent

测试

  • XCTest
let metrics: [XCTMetric] = [XCTMemoryMetric(), XCTStorageMetric(), XCTClockMetric()]

let measureOptions = XCTMeasureOptions.default
measureOptions.iterationCount = 3

measure(metrics: metrics, options: measureOptions) {
    //block of code
}

我建议您不要创建一个带有measure块的通用函数,因为Xcode有时无法正确处理它。这就是为什么应该针对每个性能测试使用measure块的原因。

1
我制作了一个小型的spm包来测量代码执行时间: https://github.com/mezhevikin/Measure.git 使用示例:
Measure.start("create-user")
let user = User()
Measure.finish("create-user")

控制台输出:

⏲ Measure [create-user]: 0.00521 sec.

测量异步请求

Measure.start("request")
let url = URL(string: "https://httpbin.org/get")!
URLSession.shared.dataTask(with: url) { _, _, _ in
    let time = Measure.finish("request")
    Analytics.send(event: "Request", ["time" => time])
}.resume()

完整包的代码:

// Mezhevikin Alexey: https://github.com/mezhevikin/Measure
import Foundation

public class Measure {
    static private var starts = [String: Double]()
    
    static public func start(_ key: String) {
        starts[key] = CFAbsoluteTimeGetCurrent()
    }

    @discardableResult
    static public func finish(_ key: String) -> Double {
        guard let start = starts[key] else {
            print(" Key [\(key)] not found")
            return 0
        }
        let time = CFAbsoluteTimeGetCurrent() - start
        print(String(format: "⏲ Measure [\(key)]: %.5f sec.", time))
        starts.removeValue(forKey: key)
        return time
    }
}

1
class TimeMeasurement {
static var shared: TimeMeasurement = TimeMeasurement()
private var start = CFAbsoluteTimeGetCurrent()
func begin() {
    start = CFAbsoluteTimeGetCurrent()
}
@discardableResult func end() -> Double {
    let diff = CFAbsoluteTimeGetCurrent() - start
    print("Took \(diff) seconds")
    return diff
}

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接