在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个回答

2

这是我尝试给出的最简单答案:

let startTime = Date().timeIntervalSince1970  // 1512538946.5705 seconds

// time passes (about 10 seconds)

let endTime = Date().timeIntervalSince1970    // 1512538956.57195 seconds
let elapsedTime = endTime - startTime         // 10.0014500617981 seconds

注意事项

  • startTimeendTime 的类型是 TimeInterval,它只是 Double 的别名,因此很容易将其转换为 Int 或其他类型。时间以秒为单位,具有亚毫秒精度。
  • 另请参阅 DateInterval,其中包括实际的开始和结束时间。
  • 使用自1970年以来的时间类似于 Java 时间戳

最简单的方法是将其分成两个字符串,第二个字符串为 _let elapsedTime = Date().timeIntervalSince1970 - startTime_。 - FreeGor

2

您可以像这样测量纳秒

let startDate: NSDate = NSDate()

// your long procedure

let endDate: NSDate = NSDate()
let dateComponents: NSDateComponents = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian).components(NSCalendarUnit.CalendarUnitNanosecond, fromDate: startDate, toDate: endDate, options: NSCalendarOptions(0))
println("runtime is nanosecs : \(dateComponents.nanosecond)")

我刚刚运行了这个程序,没有放置在“// your long procedure”中的任何代码,它给出了一个结果为240900993,即0.24秒。 - user1021430
实例化 NSCalendar 是一个昂贵的过程,但是你可以在开始运行程序之前进行实例化,这样它就不会被添加到长时间运行的过程中...请记住,调用创建 NSDate 实例和调用 –components(_:, fromDate:) 方法仍然需要时间,所以你可能永远不会得到 0.0 纳秒。 - holex
从NSCalendar构造函数的外观来看,似乎不可能提前创建它(startDate是必需的输入)。令人惊讶的是,它似乎是如此的耗费资源。 - user1021430
你可以使用以下更新后的答案,该程序在我的设备上测量时间约为6.9微秒,无需任何额外的步骤。 - holex
酷,谢谢。基本流程现在类似于JeremyP的答案,但报告方式不同。 - user1021430
如果你将我的答案中的 timeInterval 乘以 1000000000,你将得到以纳秒为单位的答案。 - JeremyP

2
使用 XCTest 中提供的 measure 函数是检查经过时间/性能的推荐方法。
编写自己的测量块不可靠,因为代码块的性能(因此执行/经过的时间)受 CPU 缓存等因素的影响。
第二次调用函数比第一次调用可能更快,尽管可能会有几个百分点的差异。因此,通过执行自己的闭包(在这里随处可见)进行“基准测试”只执行一次,可能会得到与实际用户在生产中执行代码时不同的结果。 measure 函数多次调用您的代码块,模拟您的代码在生产中使用的性能/经过时间(至少提供更准确的结果)。

1

这是一个用于基本函数计时的静态 Swift3 类。它会按名称跟踪每个计时器。在您想要开始测量的地方使用以下方式调用它:

Stopwatch.start(name: "PhotoCapture")

调用此函数以捕获并打印经过的时间:

Call this to capture and print the time elapsed:

Stopwatch.timeElapsed(name: "PhotoCapture")

这是输出结果: *** PhotoCapture 耗时毫秒数: 1402.415125 如果需要使用纳秒,可以使用"useNanos"参数。 如有需要,请随意更改。

   class Stopwatch: NSObject {

  private static var watches = [String:TimeInterval]()

  private static func intervalFromMachTime(time: TimeInterval, useNanos: Bool) -> TimeInterval {
     var info = mach_timebase_info()
     guard mach_timebase_info(&info) == KERN_SUCCESS else { return -1 }
     let currentTime = mach_absolute_time()
     let nanos = currentTime * UInt64(info.numer) / UInt64(info.denom)
     if useNanos {
        return (TimeInterval(nanos) - time)
     }
     else {
        return (TimeInterval(nanos) - time) / TimeInterval(NSEC_PER_MSEC)
     }
  }

  static func start(name: String) {
     var info = mach_timebase_info()
     guard mach_timebase_info(&info) == KERN_SUCCESS else { return }
     let currentTime = mach_absolute_time()
     let nanos = currentTime * UInt64(info.numer) / UInt64(info.denom)
     watches[name] = TimeInterval(nanos)
  }

  static func timeElapsed(name: String) {
     return timeElapsed(name: name, useNanos: false)
  }

  static func timeElapsed(name: String, useNanos: Bool) {
     if let start = watches[name] {
        let unit = useNanos ? "nanos" : "ms"
        print("*** \(name) elapsed \(unit): \(intervalFromMachTime(time: start, useNanos: useNanos))")
     }
  }

}


你整个使用 mach_timebase_info 的过程已经比你在 ProcessInfo.processInfo.systemUptime源代码中做得更好了。所以只要简单地执行 watches[name] = ProcessInfo.processInfo.systemUptime。如果你想用纳秒就加上 * TimeInterval(NSEC_PER_MSEC) - Cœur

1
将其放入完成块中以便于使用。
public class func secElapsed(completion: () -> Void) {
    let startDate: NSDate = NSDate()
    completion()
    let endDate: NSDate = NSDate()
    let timeInterval: Double = endDate.timeIntervalSinceDate(startDate)
    println("seconds: \(timeInterval)")
}

1

Swift 5+

我借鉴了Klaas的想法,创建了一个轻量级结构体来测量运行和间隔时间:

代码用法:

var timer = RunningTimer.init()
// Code to be timed
print("Running: \(timer) ") // Gives time interval
// Second code to be timed
print("Running: \(timer) ") // Gives final time

停止函数不需要被调用,因为打印函数将给出经过的时间。它可以重复调用以获取经过的时间。 但是,在代码的某个特定点停止计时器,请使用timer.stop(),它也可以用于返回时间(秒):let seconds = timer.stop() 计时器停止后,间隔计时器不会停止,因此print("Running: \(timer) ")即使在几行代码之后也会给出正确的时间。

以下是RunningTimer的代码。它已经测试过Swift 2.1:

import CoreFoundation
// Usage:    var timer = RunningTimer.init()
// Start:    timer.start() to restart the timer
// Stop:     timer.stop() returns the time and stops the timer
// Duration: timer.duration returns the time
// May also be used with print(" \(timer) ")

struct RunningTimer: CustomStringConvertible {
    var begin:CFAbsoluteTime
    var end:CFAbsoluteTime
    
    init() {
        begin = CFAbsoluteTimeGetCurrent()
        end = 0
    }

    mutating func start() {
        begin = CFAbsoluteTimeGetCurrent()
        end = 0
    }

    mutating func stop() -> Double {
        if (end == 0) { end = CFAbsoluteTimeGetCurrent() }
        return Double(end - begin)
    }

    var duration:CFAbsoluteTime {
        get {
            if (end == 0) { return CFAbsoluteTimeGetCurrent() - begin } 
            else { return end - begin }
        }
    }

    var description:String {
         let time = duration
         if (time > 100) {return " \(time/60) min"}
         else if (time < 1e-6) {return " \(time*1e9) ns"}
         else if (time < 1e-3) {return " \(time*1e6) µs"}
         else if (time < 1) {return " \(time*1000) ms"}
         else {return " \(time) s"}
    }
}

1
从Swift 5.7(macOS 13.0,iOS 16.0,watchOS 9.0,tvOS 16.0)开始,您可以使用ContinuousClockmeasure块,它返回一个Duration对象。它具有包含以秒或阿托秒为单位的测量时间的组件,即1×10−18秒。
let clock = ContinuousClock()

let duration = clock.measure {
    // put here what you want to measure
}

print("Duration: \(duration.components.seconds) seconds")
print("Duration: \(duration.components.attoseconds) attoseconds")

0

这是我写的方式。

 func measure<T>(task: () -> T) -> Double {
        let startTime = CFAbsoluteTimeGetCurrent()
        task()
        let endTime = CFAbsoluteTimeGetCurrent()
        let result = endTime - startTime
        return result
    }

要衡量一个算法,就像这样使用它。

let time = measure {
    var array = [2,4,5,2,5,7,3,123,213,12]
    array.sorted()
}

print("Block is running \(time) seconds.")

那个时间准确吗?因为我之前用计时器每0.1秒更新一次。我比较了这个答案和一个使用计时器的答案,结果是这个答案比计时器慢了0.1秒。 - Yaroslav Dukal

0

基于Franklin Yu的回答和Cœur的评论

细节

  • Xcode 10.1 (10B61)
  • Swift 4.2

解决方案1

measure(_:)

解决方案2

import Foundation

class Measurer<T: Numeric> {

    private let startClosure: ()->(T)
    private let endClosure: (_ beginningTime: T)->(T)

    init (startClosure: @escaping ()->(T), endClosure: @escaping (_ beginningTime: T)->(T)) {
        self.startClosure = startClosure
        self.endClosure = endClosure
    }

    init (getCurrentTimeClosure: @escaping ()->(T)) {
        startClosure = getCurrentTimeClosure
        endClosure = { beginningTime in
            return getCurrentTimeClosure() - beginningTime
        }
    }

    func measure(closure: ()->()) -> T {
        let value = startClosure()
        closure()
        return endClosure(value)
    }
}

解决方案 2 的使用方法

// Sample with ProcessInfo class

m = Measurer { ProcessInfo.processInfo.systemUptime }
time = m.measure {
    _ = (1...1000).map{_ in Int(arc4random()%100)}
}
print("ProcessInfo: \(time)")

// Sample with Posix clock API

m = Measurer(startClosure: {Double(clock())}) { (Double(clock()) - $0 ) / Double(CLOCKS_PER_SEC) }
time = m.measure {
    _ = (1...1000).map{_ in Int(arc4random()%100)}
}
print("POSIX: \(time)")

不确定为什么我们需要使用变量实现。 使用Date来测量任何东西都极为不建议(但systemUptimeclock()可以)。 至于测试,我们已经有了measure(_:) - Cœur
1
另外一点:为什么要将闭包设置为可选项? - Cœur
@Cœur 你说得对!感谢您的评论。我稍后会更新帖子。 - Vasily Bodnarchuk

0

这是我想出来的代码片段,它在我的Macbook上使用Swift 4似乎可以工作。

从未在其他系统上测试过,但我认为值得分享。

typealias MonotonicTS = UInt64
let monotonic_now: () -> MonotonicTS = mach_absolute_time

let time_numer: UInt64
let time_denom: UInt64
do {
    var time_info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&time_info)
    time_numer = UInt64(time_info.numer)
    time_denom = UInt64(time_info.denom)
}

// returns time interval in seconds
func monotonic_diff(from: MonotonicTS, to: MonotonicTS) -> TimeInterval {
    let diff = (to - from)
    let nanos = Double(diff * time_numer / time_denom)
    return nanos / 1_000_000_000
}

func seconds_elapsed(since: MonotonicTS) -> TimeInterval {
    return monotonic_diff(from: since, to:monotonic_now())
}

这是如何使用它的示例:
let t1 = monotonic_now()
// .. some code to run ..
let elapsed = seconds_elapsed(since: t1)
print("Time elapsed: \(elapsed*1000)ms")

另一种方法是更明确地执行:

let t1 = monotonic_now()
// .. some code to run ..
let t2 = monotonic_now()
let elapsed = monotonic_diff(from: t1, to: t2)
print("Time elapsed: \(elapsed*1000)ms")

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