如何使用 mach_absolute_time 而不会溢出?

Posted

技术标签:

【中文标题】如何使用 mach_absolute_time 而不会溢出?【英文标题】:How can I use mach_absolute_time without overflowing? 【发布时间】:2014-04-30 01:18:54 【问题描述】:

在 Darwin 上,POSIX 标准 clock_gettime(CLOCK_MONOTONIC) 计时器不可用。相反,最高分辨率的单调计时器是通过mach/mach_time.h 中的mach_absolute_time 函数获得的。

返回的结果可能是来自处理器的未经调整的滴答计数,在这种情况下,时间单位可能是一个奇怪的倍数。例如,在具有 33MHz 滴答计数的 CPU 上,Darwin 返回 1000000000/33333335 作为返回结果的精确单位(即,将mach_absolute_time 乘以该分数以获得纳秒值)。

我们通常希望将精确的刻度转换为“标准”(十进制)单位,但不幸的是,天真地将绝对时间乘以小数即使在 64 位算术中也会溢出。这是 Apple 在 mach_absolute_time 上的唯一文档属于 (Technical Q&A QA1398) 的错误。1

我应该如何编写一个正确使用mach_absolute_time的函数?


    请注意,这不是理论上的问题:QA1398 中的示例代码完全无法在基于 PowerPC 的 Mac 上运行。在 Intel Mac 上,mach_timebase_info 始终返回 1/1 作为缩放因子,因为 CPU 的原始滴答计数不可靠(动态速度步进),因此 API 会为您进行缩放。在 PowerPC Mac 上,mach_timebase_info 返回 1000000000/33333335 或 1000000000/25000000,因此 Apple 提供的代码每隔几分钟肯定会溢出。糟糕。

【问题讨论】:

我在 MacOS 10.13.6 上有 CLOCK_MONOTONIC,它填满了 timespec,返回 0;但是,不确定它是否真的是单调的。 【参考方案1】:

最精确(最佳)的答案

以 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;

如果denom=33333335mach_timebase_info 返回,我们只能在乘以数字溢出之前处理最多18 秒的差异。正如getExpressibleSpan 所示,通过为此计算一个粗略的下限,numer 的大小无关紧要:将numer 减半是maxDiffWithoutOverflow 的两倍。因此,唯一的目标是产生一个接近于 numer/denom 且分母较小的分数。最简单的方法是使用连分数。

连分数法相当方便。如果提供的区间包含一个整数,bestFrac 显然可以正常工作:它返回大于 1 的区间中的最小整数。否则,它会以严格更大的区间递归调用自身并返回 m+1/next。最终的结果是一个连分数,可以通过归纳证明它具有正确的性质:它是最优的,即在给定区间内具有最小分母的分数。

最后,当将mach_absolute_time 重新缩放到纳秒时,我们将 Darwin 传递给我们的分数减少到一个更小的分数。我们可能会在这里引入一个错误,因为我们一般不能在不损失准确性的情况下减少分数。我们为自己设定了 0.1% 的错误目标,并检查我们是否已将这一比例降低到足以正确处理常见时间跨度(最长十年)的程度。

可以说,该方法的功能过于复杂,但它可以正确处理 API 可以向其抛出的任何内容,并且生成的代码仍然很短且速度极快(bestFrac 通常只递归三到四次迭代深度之前随机间隔返回小于 1000 的分母 [a,a*1.002])。

【讨论】:

这看起来很有希望,不幸的是我似乎无法让我的编译器与那些一起工作(使用一个简单的clang foo.c)(可能是我从未见过的static struct Data Data(uint64_t bias_) : bias(bias_) 部分)你能给我一点线索吗? 它是 C++ (ObjC++),而不是 C。话虽如此,我实际上应该更新这个答案,因为我现在使用不同的技术 - 只需以高精度进行算术 :) 谢谢 :) 我也想出了一个不同的解决方案(但没有 PowerPC 来测试它是否真的有效:p) 我现在更新了我的更准确的解决方案。一年前我们在我的公司放弃了对 PPC 的支持,所以现在我们几乎可以保证永远不会看到从 mach_timebase_info() 返回的 1/1 以外的任何东西。 你错过了它是static struct Data ...,所以第一次调用函数时它只是零。之后,从以前的运行中记住偏差。【参考方案2】:

当与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;

供参考,source code of 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

作为参考,source code of 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 中,它只会丢弃结果。

【讨论】:

以上是关于如何使用 mach_absolute_time 而不会溢出?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 PNG 徽标而不是圆圈

如何使用 angular.element 而不是 jQuery?

如何使用 apache 提供脚本而不是运行它?

如何使用 OR 而不是 AND 链接范围查询?

如何将 Notepad++ 配置为使用空格而不是制表符?

如何使用端口 80 而不是 3000?