Как я могу использовать mach_absolute_time без переполнения?

В Дарвине стандартный clock_gettime(CLOCK_MONOTONIC) POSIX clock_gettime(CLOCK_MONOTONIC) недоступен. Вместо этого монотонный таймер с самым высоким разрешением получается через функцию mach_absolute_time из mach/mach_time.h.

Возвращенный результат может быть нескорректированным количеством тактов от процессора, и в этом случае единицы времени могут быть странным кратным. Например, на процессоре с тактовым отсчетом 33 МГц Дарвин возвращает 1000000000/33333335 как точные единицы возвращаемого результата (т. mach_absolute_time значение mach_absolute_time на эту дробь, чтобы получить значение наносекунды).

Обычно мы хотим преобразовать точные метки в "стандартные" (десятичные) единицы, но, к сожалению, наивное умножение абсолютного времени на дробь переполнится даже в 64-битной арифметике. Это ошибка, в mach_absolute_time попадает единственный фрагмент документации Apple по mach_absolute_time (Технические вопросы и ответы QA1398). 1

Как мне написать функцию, которая правильно использует mach_absolute_time?


  1. Обратите внимание, что это не теоретическая проблема: пример кода в QA1398 полностью не работает на Mac на базе PowerPC. На Intel Macs mach_timebase_info всегда возвращает 1/1 в качестве коэффициента масштабирования, потому что счетчик необработанных mach_timebase_info ЦП ненадежен (динамическое mach_timebase_info скорости), поэтому API выполняет масштабирование за вас. На компьютерах PowerPC Mac mach_timebase_info возвращает либо 1000000000/33333335, либо 1000000000/25000000, поэтому предоставляемый Apple код явно переполняется каждые несколько минут. К сожалению.

Ответы

Ответ 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=33333335, возвращенный mach_timebase_info, мы можем обрабатывать различия до 18 секунд только до умножения на числовые переполнения. Как показывает getExpressibleSpan, вычисляя грубую нижнюю границу для этого, размер numer не имеет значения: уменьшение вдвое numer удваивается maxDiffWithoutOverflow. Поэтому единственная цель состоит в том, чтобы создать дробь, близкую к числителю/деноту, имеющую меньший знаменатель. Самый простой способ сделать это - использовать непрерывные дроби.

Метод продолженных дробей весьма удобен. bestFrac четко работает, если предоставленный интервал содержит целое число: он возвращает наименьшее целое число в интервале более 1. В противном случае он вызывает себя рекурсивно со строго большим интервалом и возвращает m+1/next. Конечным результатом является непрерывная дробь, которая может быть показана индукцией, чтобы иметь правильное свойство: она оптимальна, доля внутри данного интервала с наименьшим знаменателем.

Наконец, мы уменьшаем долю, которую Дарвин передает нам на меньшую, которая будет использоваться при масштабировании mach_absolute_time до наносекунд. Мы можем ввести здесь ошибку, потому что мы не можем уменьшить долю вообще без потери точности. Мы поставили перед собой цель ошибки 0,1% и отметим, что мы сократили долю, достаточную для правильной обработки общих временных интервалов (до десяти лет).

Вероятно, этот метод слишком усложнен для того, что он делает, но он правильно обрабатывает все, что может на него наложить API, и полученный код все еще короткий и очень быстрый (bestFrac обычно повторяется только три или четыре итерации до возвращая знаменатель менее 1000 для случайных интервалов [a,a*1.002]).

Ответ 2

Вы беспокоитесь о переполнении при умножении/делении на значения из структуры mach_timebase_info, которая используется для преобразования в наносекунды. Таким образом, хотя он может не соответствовать вашим точным потребностям, существуют более простые способы подсчета в наносекундах или секундах.

Все приведенные ниже решения используют системные часы (а НЕ настенные часы).


Используйте 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, но не с открытым исходным кодом.


Используйте рассылку. 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()).