最精确的答案
进行128位精度的算术运算,以避免溢出!
uint64_t monotonicTimeNanos() {
uint64_t now = mach_absolute_time();
static struct Data {
Data(uint64_t bias_) : bias(bias_) {
kern_return_t mtiStatus = mach_timebase_info(&tb);
assert(mtiStatus == KERN_SUCCESS);
}
uint64_t scale(uint64_t i) {
return scaleHighPrecision(i - bias, tb.numer, tb.denom);
}
static uint64_t scaleHighPrecision(uint64_t i, uint32_t numer,
uint32_t denom) {
U64 high = (i >> 32) * numer;
U64 low = (i & 0xffffffffull) * numer / denom;
U64 highRem = ((high % denom) << 32) / denom;
high /= denom;
return (high << 32) + highRem + low;
}
mach_timebase_info_data_t tb;
uint64_t bias;
} data(now);
return data.scale(now);
}
一个简单的低分辨率答案
uint64_t monotonicTimeNanos() {
uint64_t now = mach_absolute_time();
static struct Data {
Data(uint64_t bias_) : bias(bias_) {
kern_return_t mtiStatus = mach_timebase_info(&tb);
assert(mtiStatus == KERN_SUCCESS);
if (tb.denom > 1024) {
double frac = (double)tb.numer/tb.denom;
tb.denom = 1024;
tb.numer = tb.denom * frac + 0.5;
assert(tb.numer > 0);
}
}
mach_timebase_info_data_t tb;
uint64_t bias;
} data(now);
return (now - data.bias) * data.tb.numer / data.tb.denom;
}
使用低精度算术的繁琐解决方案,但使用连分数避免精度损失
static mach_timebase_info_data_t bestFrac(double a, double b) {
if (floor(a) < floor(b))
{ mach_timebase_info_data_t rv = {(int)ceil(a), 1}; return rv; }
double m = floor(a);
mach_timebase_info_data_t next = bestFrac(1/(b-m), 1/(a-m));
mach_timebase_info_data_t rv = {(int)m*next.numer + next.denum, next.numer};
return rv;
}
uint64_t monotonicTimeNanos() {
uint64_t now = mach_absolute_time();
static struct Data {
Data(uint64_t bias_) : bias(bias_) {
kern_return_t mtiStatus = mach_timebase_info(&tb);
assert(mtiStatus == KERN_SUCCESS);
double frac = (double)tb.numer/tb.denom;
uint64_t spanTarget = 315360000000000000llu;
if (getExpressibleSpan(tb.numer, tb.denom) >= spanTarget)
return;
for (double errorTarget = 1/1024.0; errorTarget > 0.000001;) {
mach_timebase_info_data_t newFrac =
bestFrac((1-errorTarget)*frac, (1+errorTarget)*frac);
if (getExpressibleSpan(newFrac.numer, newFrac.denom) < spanTarget)
break;
tb = newFrac;
errorTarget = fabs((double)tb.numer/tb.denom - frac) / frac / 8;
}
assert(getExpressibleSpan(tb.numer, tb.denom) >= spanTarget);
}
mach_timebase_info_data_t tb;
uint64_t bias;
} data(now);
return (now - data.bias) * data.tb.numer / data.tb.denom;
}
推导
我们的目标是将mach_timebase_info
返回的分数缩小到基本相同,但分母较小的分数。我们可以处理的时间跨度大小仅受分母大小的限制,而不是我们将要乘以的分数的分子大小:
uint64_t getExpressibleSpan(uint32_t numer, uint32_t denom) {
uint64_t maxDiffWithoutOverflow = ((uint64_t)1 << (64 - ceilLog2(numer))) - 1;
return maxDiffWithoutOverflow * numer / denom;
}
如果
mach_timebase_info
返回
denom=33333335
,那么只有在乘以
numer
之前,我们才能处理多达18秒的差异。如
getExpressibleSpan
所示,通过计算此的粗略下界,
numer
的大小并不重要:将
numer
减半会使
maxDiffWithoutOverflow
翻倍。因此,唯一的目标是产生一个接近于
numer/denom
的分数,其分母更小。最简单的方法是使用连分数法。
连分数法非常方便。
bestFrac
明显在提供的区间包含整数时正确工作:它返回区间中最小的整数大于1。否则,它用严格较大的区间递归调用自身,并返回
m+1/next
。最终结果是连分数,可以归纳证明具有正确的属性:它是最优的,在给定区间内具有最小的分母。
最后,我们将Darwin传递给我们的分数缩小到一个更小的分数,以便在重新调整
mach_absolute_time
为纳秒时使用。这里可能会引入误差,因为通常情况下不能缩小分数而不失精度。我们设定了0.1%的误差目标,并检查我们是否已将分数缩小足够,在常见时间跨度(长达十年)内准确处理。
可以说这种方法对于它所做的事情有些过于复杂,但它正确处理API可能引发的任何问题,而且最终代码仍然很短且非常快速(对于随机区间
[a,a * 1.002]
,
bestFrac
通常只递归三到四次就会返回一个分母小于1000的值)。
CLOCK_MONOTONIC
,它填充 timespec 并返回 0;但我不确定它是否真的是单调的。 - kolen