了解 M1 机器上的 DispatchTime
Understand DispatchTime on M1 machines
在我的 iOS 项目中,我们能够复制 Combine's Schedulers
实施,并且我们进行了广泛的测试,在英特尔机器上一切正常,所有测试都通过了,现在我们得到了一些M1 机器,看看我们的工作流程中是否有一个 showstopper。
突然我们的一些库代码开始失败,奇怪的是即使我们使用 Combine 的实现测试仍然失败。
我们的假设是我们滥用了 DispatchTime(uptimeNanoseconds:)
正如您在下面的屏幕截图中看到的(Combine 的实现)
我们现在知道,根据文档
,使用 uptimeNanoseconds 值初始化 DispatchTime
并不意味着它们是 M1 机器上的实际纳秒
Creates a DispatchTime
relative to the system clock that
ticks since boot.
- Parameters:
- uptimeNanoseconds: The number of nanoseconds since boot, excluding
time the system spent asleep
- Returns: A new `DispatchTime`
- Discussion: This clock is the same as the value returned by
`mach_absolute_time` when converted into nanoseconds.
On some platforms, the nanosecond value is rounded up to a
multiple of the Mach timebase, using the conversion factors
returned by `mach_timebase_info()`. The nanosecond equivalent
of the rounded result can be obtained by reading the
`uptimeNanoseconds` property.
Note that `DispatchTime(uptimeNanoseconds: 0)` is
equivalent to `DispatchTime.now()`, that is, its value
represents the number of nanoseconds since boot (excluding
system sleep time), not zero nanoseconds since boot.
所以,是测试错误还是我们不应该这样使用DispatchTime
?
我们尝试遵循 Apple suggestion 并使用这个:
uint64_t MachTimeToNanoseconds(uint64_t machTime)
{
uint64_t nanoseconds = 0;
static mach_timebase_info_data_t sTimebase;
if (sTimebase.denom == 0)
(void)mach_timebase_info(&sTimebase);
nanoseconds = ((machTime * sTimebase.numer) / sTimebase.denom);
return nanoseconds;
}
没有太大帮助。
编辑:截图代码:
func testSchedulerTimeTypeDistance() {
let time1 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
let time2 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
let distantFuture = DispatchQueue.SchedulerTimeType(.distantFuture)
let notSoDistantFuture = DispatchQueue.SchedulerTimeType(
DispatchTime(
uptimeNanoseconds: DispatchTime.distantFuture.uptimeNanoseconds - 1024
)
)
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431))
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(-10001))
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(10001))
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(-10432))
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(10432))
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(-11025))
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(11025))
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(-11456))
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(11456))
XCTAssertEqual(distantFuture.distance(to: distantFuture), .nanoseconds(0))
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
.nanoseconds(0))
}
我认为你的问题在于这一行:
nanoseconds = ((machTime * sTimebase.numer) / sTimebase.denom)
...这是在做整数运算。
此处 M1 的实际比率为 125/3
(41.666...
),因此您的转换系数被截断为 41
。这是一个 ~1.6% 的错误,这可能解释了您所看到的差异。
Intel 和 ARM 代码的区别在于精度。
使用 Intel 代码,DispatchTime
在内部以纳秒为单位工作。使用 ARM 代码,它使用 纳秒 * 3 / 125(加上一些整数舍入)。同样适用于 DispatchQueue.SchedulerTimeType
.
DispatchTimeInterval
和 DispatchQueue.SchedulerTimeType.Stride
在两个平台上内部都使用纳秒。
因此 ARM 代码使用较低的精度进行计算,但在比较距离时使用全精度。此外,从纳秒转换为内部单位时会丢失精度。
DispatchTime
转换的确切公式是(作为整数运算执行):
rawValue = (nanoseconds * 3 + 124) / 125
nanoseconds = rawValue * 125 / 3
举个例子,让我们看这段代码:
let time1 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
let time2 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
计算结果:
(10000 * 3 + 124) / 125 -> 240
(10431 * 3 + 124) / 125 -> 251
251 - 240 -> 11
11 * 125 / 3 -> 458
458 和 431 之间的结果比较失败。
所以主要的解决方法是允许小的差异(我还没有验证 42 是否是最大差异):
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431), accuracy: .nanoseconds(42))
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431), accuracy: .nanoseconds(42))
还有更多惊喜:除了Intel代码外,distantFuture
和notSoDistantFuture
与ARM代码等同。它可能是这样实现的,以防止乘以 3 时发生溢出。(实际计算将是:0xFFFFFFFFFFFFFFFF * 3)。并且从内部单位到纳秒的转换将导致 0xFFFFFFFFFFFFFFFF * 125 / 3,一个大到用 64 位表示的值。
此外,我认为在计算处于或接近 0 的时间戳与处于或接近遥远未来的时间戳之间的距离时,您依赖于特定于实现的行为。测试依赖于遥远的未来在内部使用 0xFFFFFFFFFFFFFFFF 并且无符号减法环绕并产生结果,就好像内部值为 -1。
在我的 iOS 项目中,我们能够复制 Combine's Schedulers
实施,并且我们进行了广泛的测试,在英特尔机器上一切正常,所有测试都通过了,现在我们得到了一些M1 机器,看看我们的工作流程中是否有一个 showstopper。
突然我们的一些库代码开始失败,奇怪的是即使我们使用 Combine 的实现测试仍然失败。
我们的假设是我们滥用了 DispatchTime(uptimeNanoseconds:)
正如您在下面的屏幕截图中看到的(Combine 的实现)
我们现在知道,根据文档
,使用 uptimeNanoseconds 值初始化DispatchTime
并不意味着它们是 M1 机器上的实际纳秒
Creates a
DispatchTime
relative to the system clock that ticks since boot.
- Parameters:
- uptimeNanoseconds: The number of nanoseconds since boot, excluding
time the system spent asleep
- Returns: A new `DispatchTime`
- Discussion: This clock is the same as the value returned by
`mach_absolute_time` when converted into nanoseconds.
On some platforms, the nanosecond value is rounded up to a
multiple of the Mach timebase, using the conversion factors
returned by `mach_timebase_info()`. The nanosecond equivalent
of the rounded result can be obtained by reading the
`uptimeNanoseconds` property.
Note that `DispatchTime(uptimeNanoseconds: 0)` is
equivalent to `DispatchTime.now()`, that is, its value
represents the number of nanoseconds since boot (excluding
system sleep time), not zero nanoseconds since boot.
所以,是测试错误还是我们不应该这样使用DispatchTime
?
我们尝试遵循 Apple suggestion 并使用这个:
uint64_t MachTimeToNanoseconds(uint64_t machTime)
{
uint64_t nanoseconds = 0;
static mach_timebase_info_data_t sTimebase;
if (sTimebase.denom == 0)
(void)mach_timebase_info(&sTimebase);
nanoseconds = ((machTime * sTimebase.numer) / sTimebase.denom);
return nanoseconds;
}
没有太大帮助。
编辑:截图代码:
func testSchedulerTimeTypeDistance() {
let time1 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
let time2 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
let distantFuture = DispatchQueue.SchedulerTimeType(.distantFuture)
let notSoDistantFuture = DispatchQueue.SchedulerTimeType(
DispatchTime(
uptimeNanoseconds: DispatchTime.distantFuture.uptimeNanoseconds - 1024
)
)
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431))
XCTAssertEqual(time1.distance(to: distantFuture), .nanoseconds(-10001))
XCTAssertEqual(distantFuture.distance(to: time1), .nanoseconds(10001))
XCTAssertEqual(time2.distance(to: distantFuture), .nanoseconds(-10432))
XCTAssertEqual(distantFuture.distance(to: time2), .nanoseconds(10432))
XCTAssertEqual(time1.distance(to: notSoDistantFuture), .nanoseconds(-11025))
XCTAssertEqual(notSoDistantFuture.distance(to: time1), .nanoseconds(11025))
XCTAssertEqual(time2.distance(to: notSoDistantFuture), .nanoseconds(-11456))
XCTAssertEqual(notSoDistantFuture.distance(to: time2), .nanoseconds(11456))
XCTAssertEqual(distantFuture.distance(to: distantFuture), .nanoseconds(0))
XCTAssertEqual(notSoDistantFuture.distance(to: notSoDistantFuture),
.nanoseconds(0))
}
我认为你的问题在于这一行:
nanoseconds = ((machTime * sTimebase.numer) / sTimebase.denom)
...这是在做整数运算。
此处 M1 的实际比率为 125/3
(41.666...
),因此您的转换系数被截断为 41
。这是一个 ~1.6% 的错误,这可能解释了您所看到的差异。
Intel 和 ARM 代码的区别在于精度。
使用 Intel 代码,DispatchTime
在内部以纳秒为单位工作。使用 ARM 代码,它使用 纳秒 * 3 / 125(加上一些整数舍入)。同样适用于 DispatchQueue.SchedulerTimeType
.
DispatchTimeInterval
和 DispatchQueue.SchedulerTimeType.Stride
在两个平台上内部都使用纳秒。
因此 ARM 代码使用较低的精度进行计算,但在比较距离时使用全精度。此外,从纳秒转换为内部单位时会丢失精度。
DispatchTime
转换的确切公式是(作为整数运算执行):
rawValue = (nanoseconds * 3 + 124) / 125
nanoseconds = rawValue * 125 / 3
举个例子,让我们看这段代码:
let time1 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10000))
let time2 = DispatchQueue.SchedulerTimeType(.init(uptimeNanoseconds: 10431))
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431))
计算结果:
(10000 * 3 + 124) / 125 -> 240
(10431 * 3 + 124) / 125 -> 251
251 - 240 -> 11
11 * 125 / 3 -> 458
458 和 431 之间的结果比较失败。
所以主要的解决方法是允许小的差异(我还没有验证 42 是否是最大差异):
XCTAssertEqual(time1.distance(to: time2), .nanoseconds(431), accuracy: .nanoseconds(42))
XCTAssertEqual(time2.distance(to: time1), .nanoseconds(-431), accuracy: .nanoseconds(42))
还有更多惊喜:除了Intel代码外,distantFuture
和notSoDistantFuture
与ARM代码等同。它可能是这样实现的,以防止乘以 3 时发生溢出。(实际计算将是:0xFFFFFFFFFFFFFFFF * 3)。并且从内部单位到纳秒的转换将导致 0xFFFFFFFFFFFFFFFF * 125 / 3,一个大到用 64 位表示的值。
此外,我认为在计算处于或接近 0 的时间戳与处于或接近遥远未来的时间戳之间的距离时,您依赖于特定于实现的行为。测试依赖于遥远的未来在内部使用 0xFFFFFFFFFFFFFFFF 并且无符号减法环绕并产生结果,就好像内部值为 -1。