如何在不溢出的情况下使用mach_absolute_time?

18
在Darwin系统上,POSIX标准下的 clock_gettime(CLOCK_MONOTONIC) 定时器不可用。因此,最高分辨率的单调定时器需要通过从 mach/mach_time.h 导入 mach_absolute_time 函数来获得。
返回的结果可能是处理器中未校准的滴答计数,此时时间单位可能是奇怪的倍数。例如,在33MHz的处理器上,Darwin将1000000000/33333335作为返回结果的精确单位(即将 mach_absolute_time 乘以该分数以获取纳秒值)。
通常,我们希望从精确滴答转换为“标准”(十进制)单位,但遗憾的是,简单地将绝对时间乘以分数将在64位算术中溢出。这是苹果公司唯一有关 mach_absolute_time 的文件中掉入的错误(Technical Q&A QA1398)。
我应该如何编写一个正确使用 mach_absolute_time 的函数?
请注意,这不是一个理论性问题:QA1398中的示例代码在基于PowerPC的Mac上完全无法工作。在英特尔Mac上,mach_timebase_info 总是返回1/1作为缩放因子,因为处理器的原始滴答计数是不可靠的(动态速度调整),所以API会为您完成缩放。在基于PowerPC的Mac上,mach_timebase_info 返回1000000000/33333335或1000000000/25000000,因此苹果提供的代码肯定会每隔几分钟溢出。糟糕。

我在 MacOS 10.13.6 上有 CLOCK_MONOTONIC,它填充 timespec 并返回 0;但我不确定它是否真的是单调的。 - kolen
3个回答

26

最精确的答案

进行128位精度的算术运算,以避免溢出!

// Returns monotonic time in nanos, measured from the first time the function
// is called in the process.
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);
}

一个简单的低分辨率答案

// Returns monotonic time in nanos, measured from the first time the function
// is called in the process.  The clock may run up to 0.1% faster or slower
// than the "exact" tick count.
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;
}

使用低精度算术的繁琐解决方案,但使用连分数避免精度损失

// This function returns the rational number inside the given interval with
// the smallest denominator (and smallest numerator breaks ties; correctness
// proof neglects floating-point errors).
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;
}

// Returns monotonic time in nanos, measured from the first time the function
// is called in the process.  The clock may run up to 0.1% faster or slower
// than the "exact" tick count. However, although the bound on the error is
// the same as for the pragmatic answer, the error is actually minimized over
// the given accuracy bound.
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; // 10 years
      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) {
  // This is just less than the smallest thing we can multiply numer by without
  // overflowing. ceilLog2(numer) = 64 - number of leading zeros of numer
  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的值)。

看起来很有前途,不幸的是我似乎无法让我的编译器与它们一起工作(使用简单的clang foo.c)(可能是我从未见过的static struct Data {Data(uint64_t bias_) : bias(bias_) {部分),你能给我任何提示吗? - Romuald Brunet
这是C++(ObjC++),不是C。 话虽如此,我应该更新一下这个答案,因为我现在使用一种不同的技术 - 只需在高精度下进行算术运算 :) - Nicholas Wilson
谢谢 :) 我也想到了另一种解决方案(但是没有PowerPC来测试它是否真正可行:p) - Romuald Brunet
我现在已经更新了更准确的解决方案。我们公司一年前就停止支持PPC了,所以现在这些都是无用的,因为我们几乎可以保证从mach_timebase_info()返回的只有1/1 - Nicholas Wilson
在标题为“执行128位精度的算术以避免溢出!”的代码中,now被传递给Data的构造函数,该函数将bias设置为now的值,然后将now传递给scale,在那里从i中减去了bias。这不意味着nownow中减去吗?...我看不出这不总是零的原因? - tuple_cat
1
你忽略了它是 static struct Data ...,所以它只在第一次调用函数时清零。之后偏差会从之前的运行中记住。 - Nicholas Wilson

4
您正在担心使用mach_timebase_info结构体中的值进行乘除时可能会发生溢出问题,该结构体用于转换为纳秒。因此,虽然它可能不完全符合您的需求,但有更简单的方法来获取纳秒或秒的计数。
以下所有解决方案都在内部使用mach_absolute_time(而不是壁钟)。

使用double而不是uint64_t

(支持Objective-C和Swift)

double tbInSeconds = 0;
mach_timebase_info_data_t tb;
kern_return_t kError = mach_timebase_info(&tb);
if (kError == 0) {
    tbInSeconds = 1e-9 * (double)tb.numer / (double)tb.denom;
}

如果你需要纳秒级别的精度,请移除代码中的1e-9

用法:

uint64_t start = mach_absolute_time();
// do something
uint64_t stop = mach_absolute_time();
double durationInSeconds = tbInSeconds * (stop - start);

使用ProcessInfo.processInfo.systemUptime

(支持Objective-C和Swift)

它可以直接以double秒为单位完成工作:

CFTimeInterval start = NSProcessInfo.processInfo.systemUptime;
// do something
CFTimeInterval stop = NSProcessInfo.processInfo.systemUptime;
NSTimeInterval durationInSeconds = stop - start;

供参考,systemUptime的源代码与之前的解决方案类似:

struct mach_timebase_info info;
mach_timebase_info(&info);
__CFTSRRate = (1.0E9 / (double)info.numer) * (double)info.denom;
__CF1_TSRRate = 1.0 / __CFTSRRate;
uint64_t tsr = mach_absolute_time();
return (CFTimeInterval)((double)tsr * __CF1_TSRRate);

使用QuartzCore.CACurrentMediaTime()

(支持Objective-C和Swift)

systemUptime相同,但不是开源的。


使用Dispatch.DispatchTime.now()

(仅支持Swift)

另一个封装了mach_absolute_time()的方法。基本精度为纳秒,使用UInt64支持。

DispatchTime start = DispatchTime.now()
// do something
DispatchTime stop = DispatchTime.now()
TimeInterval durationInSeconds = Double(end.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000

参考一下,DispatchTime.now()的源代码说它基本上只返回一个结构体DispatchTime(rawValue: mach_absolute_time())。计算uptimeNanoseconds的方法如下:

(result, overflow) = result.multipliedReportingOverflow(by: UInt64(DispatchTime.timebaseInfo.numer))
result = overflow ? UInt64.max : result / UInt64(DispatchTime.timebaseInfo.denom)

如果乘法结果无法存储在UInt64中,它就会丢弃结果。


0

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