在Swift中测量经过的时间

183

如何在Swift中测量运行函数所用的时间?我想要显示经过的时间,例如:“经过了0.05秒”。在Java中看到可以使用System.nanoTime(),在Swift中是否有等效的方法来完成这个任务?

请查看示例程序:

func isPrime(_ number: Int) -> Bool {
    var i = 0;
    for i=2; i<number; i++ {
        if number % i == 0, i != 0 {
            return false
        }
    }
    return true
}

var number = 5915587277

if isPrime(number) {
    print("Prime number")
} else {
    print("NOT a prime number")
}

1
这与您的时间测量问题无关,但是循环可以在 sqrt(number) 处停止,而不是在 number 处停止,这样可以节省更多时间 - 但是还有更多优化寻找质数的想法。 - holex
1
@holex 请忽略所使用的算法。我想弄清楚如何测量经过的时间? - jay
1
你可以使用 NSDate 对象并且可以测量它们之间的差异。 - holex
4
如果你正在使用XCode,我建议你使用新的性能测试功能。它可以为你完成所有重活,甚至多次运行并计算平均时间和标准差。 - Roshan
@Roshan 虽然这可能不是一个答案,但我很想看到您对Xcode性能测试功能的评论扩展。我在哪里可以了解更多信息?我如何使用它来比较各种方法所需的运行时间? - dumbledad
1
@dumbledad 您可以直接在单元测试中测量整个代码块的性能。例如,参考这个链接。如果您想进一步细分运行(比如逐行代码),请查看 Time Profiler,它是 Instruments 的一部分。这种方法更加强大和全面。 - Roshan
20个回答

318

更新

随着Swift 5.7的推出,下面的内容变得过时了。Swift 5.7引入了一个Clock的概念,该函数的设计正好可以满足此处所需。

提供了两个具体的Clock示例:ContinuousClockSuspendingClock。前者在系统挂起时继续运行,而后者则不会。

以下是在Swift 5.7中要做的示例。

func doSomething()
{
    for i in 0 ..< 1000000
    {
        if (i % 10000 == 0)
        {
            print(i)
        }
    }
}

let clock = ContinuousClock()

let result = clock.measure(doSomething)

print(result) // On my laptop, prints "0.552065882 seconds"


当然,它还可以直接测量闭合。
let clock = ContinuousClock()

let result = clock.measure {
    for i in 0 ..< 1000000
    {
        if (i % 10000 == 0)
        {
            print(i)
        }
    }
}

print(result) // "0.534663798 seconds"

Swift 5.7之前

这是我用Swift编写的一个函数,用于测量Project Euler问题。

自Swift 3以来,现在有一个“swiftified”版本的Grand Central Dispatch。因此,正确的答案可能是使用DispatchTime API

我的函数大致如下:

// Swift 3
func evaluateProblem(problemNumber: Int, problemBlock: () -> Int) -> Answer
{
    print("Evaluating problem \(problemNumber)")

    let start = DispatchTime.now() // <<<<<<<<<< Start time
    let myGuess = problemBlock()
    let end = DispatchTime.now()   // <<<<<<<<<<   end time

    let theAnswer = self.checkAnswer(answerNum: "\(problemNumber)", guess: myGuess)

    let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds // <<<<< Difference in nano seconds (UInt64)
    let timeInterval = Double(nanoTime) / 1_000_000_000 // Technically could overflow for long running tests

    print("Time to evaluate problem \(problemNumber): \(timeInterval) seconds")
    return theAnswer
}

旧答案

对于Swift 1和2,我的函数使用NSDate:

// Swift 1
func evaluateProblem(problemNumber: Int, problemBlock: () -> Int) -> Answer
{
    println("Evaluating problem \(problemNumber)")

    let start = NSDate() // <<<<<<<<<< Start time
    let myGuess = problemBlock()
    let end = NSDate()   // <<<<<<<<<<   end time

    let theAnswer = self.checkAnswer(answerNum: "\(problemNumber)", guess: myGuess)

    let timeInterval: Double = end.timeIntervalSinceDate(start) // <<<<< Difference in seconds (double)

    println("Time to evaluate problem \(problemNumber): \(timeInterval) seconds")
    return theAnswer
}

请注意,使用NSdate进行时间函数操作是不建议的:“系统时间可能会因与外部时间参考的同步或时钟的显式用户更改而减少。”。

6
Swift3 对我来说不起作用,它返回了 0.0114653027,而我确切知道这是不正确的。带有日期的那个返回结果为 4.77720803022385 秒。 - Cristi Băluță
@CristiBăluță 有没有可能你能进行一些调试,看看哪里出了问题?我在代码中没有看到明显的错误。 - JeremyP
7
很不幸,情况并不如此。由于某种原因,运行时间完全超出了预期。我的测试显示花费了1004959766纳秒(约1秒),但实际上肯定持续了约40秒。我同时使用了“Date”,结果它报告了41.87秒。我不知道“uptimeNanoseconds”在做什么,但它没有报告正确的持续时间。 - jowie
4
非常抱歉这次大幅修改,优先考虑您的新解决方案,但我强烈建议完全删除您的NSDate代码:使用NSDate是非常不可靠的。NSDate基于系统时钟,由于许多不同原因,例如网络时间同步(NTP)更新时钟(经常发生以进行漂移调整),DST调整,闰秒和用户手动调整时钟,系统时钟可能随时发生变化。此外,您不能再发布任何使用Swift 1的应用程序到AppStore上:至少需要使用Swift 3。因此,除了说“不要这样做”之外,保留NSDate代码毫无意义。 - Cœur
1
@Cœur 你对NSDate版本的批评是正确的(除了夏令时的改变不会影响NSDate,因为它始终处于GMT时间),而且你对顺序进行的修改确实是一种改进。 - JeremyP
显示剩余14条评论

85
这是一个基于CoreFoundation的方便的计时器类,它使用了CFAbsoluteTime
import CoreFoundation

class ParkBenchTimer {
    let startTime: CFAbsoluteTime
    var endTime: CFAbsoluteTime?

    init() {
        startTime = CFAbsoluteTimeGetCurrent()
    }

    func stop() -> CFAbsoluteTime {
        endTime = CFAbsoluteTimeGetCurrent()

        return duration!
    }

    var duration: CFAbsoluteTime? {
        if let endTime = endTime {
            return endTime - startTime
        } else {
            return nil
        }
    }
}

你可以像这样使用它:

let timer = ParkBenchTimer()

// ... a long runnig task ...

print("The task took \(timer.stop()) seconds.")

4
根据其文档,重复调用此函数不能保证结果单调增加。 - Franklin Yu
10
多次调用该函数并不保证结果单调递增。系统时间可能因为与外部时间参考的同步或用户显式更改时钟而减小。 - Klaas
开发者应该考虑“与外部时间参考的同步”和“时钟的显式用户更改”。您是否意味着这两种情况不可能发生? - Franklin Yu
@FranklinYu 不,我只是想说明这是两个可能的原因。如果你依赖于始终获得单调递增的值,还有其他选项。 - Klaas
有时候我希望测量一个短时间段;如果在此期间发生同步,我可能会得到负时间。实际上有一个 mach_absolute_time 函数可以避免这个问题,所以我想知道为什么它不是广为人知的。也许只是因为它没有很好的文档说明。 - Franklin Yu
这是一个带有某种相关语义的很酷的名称。谢谢。 - Yevhen Dubinin

73

使用 clockProcessInfo.systemUptime 或者 DispatchTime 来简单地测量启动时间。


据我所知,至少有十种方法来测量经过的时间:

基于单调时钟:

  1. ProcessInfo.systemUptime
  2. mach_absolute_time,如这个答案中所提到的mach_timebase_info
  3. clock()POSIX标准中。
  4. times()POSIX标准中。(过于复杂,因为我们需要考虑用户时间与系统时间之间的区别,并且子进程也涉及在内)
  5. DispatchTime(封装了Mach时间API),如被接受的答案中JeremyP所提到的。
  6. CACurrentMediaTime()

基于墙上时钟:

(不要用来做指标测量:下面说明原因)

  1. NSDate/Date,其他人提到过。
  2. CFAbsoluteTime,其他人提到过。
  3. DispatchWallTime
  4. gettimeofday()POSIX标准中。

选项1、2和3将在下面进行详细阐述。

选项1:Foundation中的Process Info API

do {
    let info = ProcessInfo.processInfo
    let begin = info.systemUptime
    // do something
    let diff = (info.systemUptime - begin)
}

其中diff:NSTimeInterval表示经过的时间(以秒为单位)。

选项2:Mach C API

do {
    var info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&info)
    let begin = mach_absolute_time()
    // do something
    let diff = Double(mach_absolute_time() - begin) * Double(info.numer) / Double(info.denom)
}

diff:Double表示经过纳秒计算的时间差。

选项3:POSIX时钟API

do {
    let begin = clock()
    // do something
    let diff = Double(clock() - begin) / Double(CLOCKS_PER_SEC)
}

diff:Double是经过秒计算的已经过去的时间。

为什么不使用挂钟时间来计算已过时间?

CFAbsoluteTimeGetCurrent文档中:

多次调用此函数不能保证递增的结果。

原因与Java中的currentTimeMillisnanoTime类似:

你不能把其中一个用于另一个目的。原因是因为没有计算机时钟是完美的;它总是会漂移并偶尔需要校准。这种校准可能是手动的,或者在大多数机器上,有一个进程运行并持续发出系统时钟(“挂钟”)的小修正。这些往往经常发生。每当有闰秒时也会发生这样的校正。

在这里,CFAbsoluteTime提供的是挂钟时间而不是启动时间。 NSDate也是挂钟时间。


1
最佳答案是什么?但是该采取什么最好的方法呢?! - Fattie
1
选项5最适合我。如果您考虑多平台代码,请尝试这个。Darwin和Glibc库都有clock()函数。 - Misha Svyatoshenko
对于基于系统时钟的解决方案:我成功地使用了ProcessInfo.processInfo.systemUptimemach_absolute_time()DispatchTime.now()CACurrentMediaTime()。但是使用clock()的解决方案无法正确转换为秒。因此,我建议将您的第一句话更新为:“请使用ProcessInfo.systemUptime或DispatchTime获取准确的启动时间。 - Cœur
很棒的答案。解释得非常清楚。 - KSR
很好的答案。如果可以的话,选项2可以使用clock_gettime_nsec_np(CLOCK_UPTIME_RAW) 进行优化,它会为您进行时间基准转换并提供纳秒值(请参见 http://www.manpagez.com/man/3/clock_gettime_nsec_np/)。 - oscaroscar

58

Swift 4最短的答案:

let startingPoint = Date()
//  ... intensive task
print("\(startingPoint.timeIntervalSinceNow * -1) seconds elapsed")

它将会打印出一些类似于1.02107906341553 seconds elapsed的内容(当然,时间会根据任务而异,我只是展示这个测量的小数精度水平给大家看看)。

希望对Swift 4有所帮助!

更新

如果你想要一种通用的测试代码片段的方法,我建议使用下面的代码:

func measureTime(for closure: @autoclosure () -> Any) {
    let start = CFAbsoluteTimeGetCurrent()
    closure()
    let diff = CFAbsoluteTimeGetCurrent() - start
    print("Took \(diff) seconds")
}

使用方法

measureTime(for: <insert method signature here>)

控制台日志

Took xx.xxxxx seconds

2
有人能解释一下为什么要 * -1 吗? - Yaroslav Dukal
3
因为它是负数,所以人们想要正数! - thachnb
2
@thachnb 我不知道 "startingPoint.timeIntervalSinceNow" 会产生负值... - Yaroslav Dukal
3
苹果公司表示:如果日期对象早于当前日期和时间,则该属性值为负。 - Peter Guan

20

只需复制并粘贴此函数。用 Swift 5 编写。这里是 JeremyP 的代码。

func calculateTime(block : (() -> Void)) {
        let start = DispatchTime.now()
        block()
        let end = DispatchTime.now()
        let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds
        let timeInterval = Double(nanoTime) / 1_000_000_000
        print("Time: \(timeInterval) seconds")
    }

像这样使用

calculateTime {
     exampleFunc()// function whose execution time to be calculated
}

我试图做类似的事情,但最终出现了算术溢出错误! - Siddharth Choudhary

18
let start = NSDate()

for index in 1...10000 {
    // do nothing
}

let elapsed = start.timeIntervalSinceNow

// elapsed is a negative value.

4
abs(start.timeIntervalSinceNow)//<--正值该代码片段的意思是计算从开始时间到现在经过的时间差,并返回一个正数。其中,"abs"表示取绝对值函数,"start"为起始时间,".timeIntervalSinceNow"表示计算当前时间与起始时间之间的时间差。 - Sentry.co
这是我见过的最直接的解决方案,非常感谢。 - MiladiuM

6
你可以创建一个time函数来测量你的调用时间。我受到Klaas'回答的启发。
func time <A> (f: @autoclosure () -> A) -> (result:A, duration: String) {
    let startTime = CFAbsoluteTimeGetCurrent()
    let result = f()
    let endTime = CFAbsoluteTimeGetCurrent()
    return (result, "Elapsed time is \(endTime - startTime) seconds.")
}

这个函数可以这样调用:time (isPrime(7)),返回一个包含结果和经过时间描述的元组。
如果只需要经过时间,可以这样:time (isPrime(7)).duration

3
根据文档,对此函数进行重复调用不能保证结果单调递增。 - Franklin Yu

6

看起来 iOS 13 引入了一种新的API,可与 DispatchTime 一起使用,不再需要手动计算两个时间戳之间的差异。

distance(to:)

let start: DispatchTime = .now()
heavyTaskToMeasure()
let duration = start.distance(to: .now())
print(duration) 
// prints: nanoseconds(NUMBER_OF_NANOSECONDS_BETWEEN_TWO_TIMESTAMPS)

遗憾的是没有提供文档,但在进行了一些测试后,.nanoseconds 情况似乎总是返回。

通过一个简单的扩展,可以将 DispatchTimeInterval 转换为 TimeInterval参考来源

extension TimeInterval {
    init?(dispatchTimeInterval: DispatchTimeInterval) {
        switch dispatchTimeInterval {
        case .seconds(let value):
            self = Double(value)
        case .milliseconds(let value):
            self = Double(value) / 1_000
        case .microseconds(let value):
            self = Double(value) / 1_000_000
        case .nanoseconds(let value):
            self = Double(value) / 1_000_000_000
        case .never:
            return nil
        }
    }
}

3

使用闭包的简单助手函数,用于测量执行时间。

func printExecutionTime(withTag tag: String, of closure: () -> ()) {
    let start = CACurrentMediaTime()
    closure()
    print("#\(tag) - execution took \(CACurrentMediaTime() - start) seconds")
}

使用方法:

printExecutionTime(withTag: "Init") {
    // Do your work here
}

结果: #初始化 - 执行花费了1.00104497105349秒


2

我使用这个:

public class Stopwatch {
    public init() { }
    private var start_: NSTimeInterval = 0.0;
    private var end_: NSTimeInterval = 0.0;

    public func start() {
        start_ = NSDate().timeIntervalSince1970;
    }

    public func stop() {
        end_ = NSDate().timeIntervalSince1970;
    }

    public func durationSeconds() -> NSTimeInterval {
        return end_ - start_;
    }
}

我不确定这个翻译是否比以前发布的更准确。但是秒数有很多小数位,似乎能够捕捉到算法中的小代码更改,例如使用swap()与实现自己的swap等。

记得在测试性能时提高你的构建优化:

Swift编译器优化


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