Монотонные часы на OSX

CLOCK_MONOTONIC недоступен, поэтому clock_gettime отсутствует.

Я читал в некоторых местах, что mach_absolute_time() может быть правильным способом, но после прочтения, что это "значение, зависящее от процессора", это мгновенно заставило меня задуматься, использует ли он rtdsc под ним. Таким образом, значение может дрейфовать со временем, даже если оно монотонно. Кроме того, проблемы с привязкой к потоку могут привести к значительным разным результатам от вызова функции (делая ее не монотонной во всех ядрах).

Конечно, это просто спекуляция. Кто-нибудь знает, как работает mach_absolute_time? Я действительно ищу замену clock_gettime (CLOCK_MONOTONIC... или что-то вроде этого для OSX. Независимо от того, что является источником синхронизации, я ожидаю как минимум миллисекундную точность и миллисекундную точность.

Я просто хотел бы понять, какие часы доступны, какие часы монотонны, если некоторые дрифты часов, проблемы с привязкой к потоку, не поддерживаются на всех аппаратных средствах Mac или берут "супервысокое" количество циклов процессора выполнить.

Вот ссылки, которые я смог найти по этой теме (некоторые из них уже мертвые ссылки и не найдены на archive.org):

https://developer.apple.com/library/mac/#qa/qa1398/_index.html http://www.wand.net.nz/~smr26/wordpress/2009/01/19/monotonic-time-in-mac-os-x/ http://www.meandmark.com/timing.pdf

Спасибо! Бретт

Ответ 1

Ядро Mach обеспечивает доступ к системным часам, из которых хотя бы один (SYSTEM_CLOCK) объявленный в документации, монотонно увеличивается.

#include <mach/clock.h>
#include <mach/mach.h>

clock_serv_t cclock;
mach_timespec_t mts;

host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock);
clock_get_time(cclock, &mts);
mach_port_deallocate(mach_task_self(), cclock);

mach_timespec_t имеет наносекундную точность. Однако я не уверен в точности.

Mac OS X поддерживает три такта:

  • SYSTEM_CLOCK возвращает время с момента загрузки;
  • CALENDAR_CLOCK возвращает время UTC с 1970-01-01;
  • REALTIME_CLOCK является устаревшим и является таким же, как SYSTEM_CLOCK в его текущей реализации.

В документации для clock_get_time указано, что часы монотонно увеличиваются, если только кто-то не называет clock_set_time. Вызов clock_set_time обескуражен, поскольку он может нарушить монотонное свойство часов, а на самом деле возвращает текущую реализацию KERN_FAILURE, ничего не делая.

Ответ 2

Посмотрев на несколько разных ответов, я закончил определение заголовка, который эмулирует clock_gettime на mach:

#include <sys/types.h>
#include <sys/_types/_timespec.h>
#include <mach/mach.h>
#include <mach/clock.h>

#ifndef mach_time_h
#define mach_time_h

/* The opengroup spec isn't clear on the mapping from REALTIME to CALENDAR
 being appropriate or not.
 http://pubs.opengroup.org/onlinepubs/009695299/basedefs/time.h.html */

// XXX only supports a single timer
#define TIMER_ABSTIME -1
#define CLOCK_REALTIME CALENDAR_CLOCK
#define CLOCK_MONOTONIC SYSTEM_CLOCK

typedef int clockid_t;

/* the mach kernel uses struct mach_timespec, so struct timespec
    is loaded from <sys/_types/_timespec.h> for compatability */
// struct timespec { time_t tv_sec; long tv_nsec; };

int clock_gettime(clockid_t clk_id, struct timespec *tp);

#endif

и в mach_gettime.c

#include "mach_gettime.h"
#include <mach/mach_time.h>

#define MT_NANO (+1.0E-9)
#define MT_GIGA UINT64_C(1000000000)

// TODO create a list of timers,
static double mt_timebase = 0.0;
static uint64_t mt_timestart = 0;

// TODO be more careful in a multithreaded environement
int clock_gettime(clockid_t clk_id, struct timespec *tp)
{
    kern_return_t retval = KERN_SUCCESS;
    if( clk_id == TIMER_ABSTIME)
    {
        if (!mt_timestart) { // only one timer, initilized on the first call to the TIMER
            mach_timebase_info_data_t tb = { 0 };
            mach_timebase_info(&tb);
            mt_timebase = tb.numer;
            mt_timebase /= tb.denom;
            mt_timestart = mach_absolute_time();
        }

        double diff = (mach_absolute_time() - mt_timestart) * mt_timebase;
        tp->tv_sec = diff * MT_NANO;
        tp->tv_nsec = diff - (tp->tv_sec * MT_GIGA);
    }
    else // other clk_ids are mapped to the coresponding mach clock_service
    {
        clock_serv_t cclock;
        mach_timespec_t mts;

        host_get_clock_service(mach_host_self(), clk_id, &cclock);
        retval = clock_get_time(cclock, &mts);
        mach_port_deallocate(mach_task_self(), cclock);

        tp->tv_sec = mts.tv_sec;
        tp->tv_nsec = mts.tv_nsec;
    }

    return retval;
}

Ответ 3

Просто используйте Время Маха.
Это открытый API, он работает на macOS, iOS и tvOS, и он работает из песочницы.

Mach Time возвращает абстрактную единицу времени, которую я обычно называю "тиканием часов". Длительность тактового сигнала зависит от системы и зависит от процессора. На современных системах Intel тактовый сигнал на самом деле составляет ровно одну наносекунду, но вы не можете положиться на это (может быть, для ARM это может быть иначе, и для процессоров PowerPC это было совсем другое). Система также может рассказать вам о конверсионном коэффициенте для преобразования тактовых импульсов на наносекунды и наносекунды на тактовые сигналы (этот показатель является статичным, он не будет меняться во время выполнения). Когда ваша система загружается, часы начинаются с 0, а затем монотонно увеличивается с каждым тактом после этого, поэтому вы также можете использовать Mach Time для обеспечения бесперебойной работы вашей системы (и, конечно же, время безотказной работы является монотонным!).

Вот код:

#include <stdio.h>
#include <inttypes.h>
#include <mach/mach_time.h>

int main ( ) {
    uint64_t clockTicksSinceSystemBoot = mach_absolute_time();
    printf("Clock ticks since system boot: %"PRIu64"\n",
        clockTicksSinceSystemBoot
    );

    static mach_timebase_info_data_t timebase;
    mach_timebase_info(&timebase);
    // Cast to double is required to make this a floating point devision,
    // otherwise it would be an interger division and only the result would
    // be converted to floating point!
    double clockTicksToNanosecons = (double)timebase.numer / timebase.denom;

    uint64_t systemUptimeNanoseconds = (uint64_t)(
        clockTicksToNanosecons * clockTicksSinceSystemBoot
    );
    uint64_t systemUptimeSeconds = systemUptimeNanoseconds / (1000 * 1000 * 1000);
    printf("System uptime: %"PRIu64" seconds\n", systemUptimeSeconds);
}

Вы также можете поместить поток в режим сна до тех пор, пока не будет достигнуто определенное время Маха. Вот какой код для этого:

// Sleep for 750 ns
uint64_t machTimeNow = mach_absolute_time();
uint64_t clockTicksToSleep = (uint64_t)(750 / clockTicksToNanosecons);
uint64_t machTimeIn750ns = machTimeNow + clockTicksToSleep;
mach_wait_until(machTimeIn750ns);

Поскольку Mach Time не имеет никакого отношения к какому-либо времени настенного времени, вы можете поиграть со своими системными настройками даты и времени по своему усмотрению, что не повлияет на время Mach.

Тем не менее, существует особое соображение, которое может сделать Mach Time непригодным для определенных случаев использования: T CPU не работает, пока ваша система спит! Поэтому, если вы создаете поток, подождите 5 минуты и через 1 минуту система переходит в спящий режим и остается спать в течение 30 минут, нить все еще ждет еще 4 минуты после того, как система проснулась, так как 30-минутное время сна не считается! В то же время часы процессора тоже отдыхали. Но в других случаях это именно то, что вы хотите.

Mach Time - также очень точный способ измерения затраченного времени. Вот какой код показывает эту задачу:

// Measure time
uint64_t machTimeBegin = mach_absolute_time();
sleep(1);
uint64_t machTimeEnd = mach_absolute_time();
uint64_t machTimePassed = machTimeEnd - machTimeBegin;
uint64_t timePassedNS = (uint64_t)(
    machTimePassed * clockTicksToNanosecons
);
printf("Thread slept for: %"PRIu64" ns\n", timePassedNS);

Вы увидите, что поток не спящий ровно на одну секунду, потому что требуется некоторое время, чтобы поместить поток в режим сна, чтобы он снова встал на задний план и даже когда он проснулся, он не получит сразу процессорное время если все ядра уже заняты потоком в этот момент.