Standard Time как его видит IBM

6a93e10b6cf7b4a10831773778302d7e.jpeg

Есть многое в природе, друг Горацио, Что и не снилось нашим мудрецам.© Вильям Шекспир

Более 6-ти лет занимаюсь разработкой под IBM i (бывшая AS/400). В основном, конечно, это работа с БД и разная бизнес-логика, но иногда приходится и что-то низкоуровневое писать.

Не так давно занимался разработкой удобного и простого в использовании API для работы с User Queue (есть на АС-ке такой системный объект — очередь *USRQ). И в заголовке извлекаемого из очереди сообщения есть такой параметр — message enqueue time (время размещения сообщения в очереди). Который описан как _MI_Time. Которое, в свою очередь, определено как

    /* The standard definition for time in the MI library:           */
    typedef char _MI_Time[8];

MI в данном случае — это Machine Instructions. То, что на АС-ке вместо ассемблера (на верхнем уровне). Набор низкоуровневых команд для различных операций. Для многих MI в С-шной библиотеке есть соответствующие врапперы. В частности, этот самый _MI_Time можно получить функцией

    /* Materialize Time of Day   */
    void mattod  ( _MI_Time );           /* Time-of-day template       */

Или более универсальной (и, как ни странно, более быстрой)

   /* Materialize Machine Data  */
   void matmdata ( _SPCPTR,             /* Machine data template  @A3C*/
                   short );             /* Options                    */

с соответствующим флагом в поле Options.

Но вот вопрос -, а что это такое и что с ним делать? Как получить из этого какое-то осмысленное время?

Есть, конечно, несколько системных API, конвертирующих это время во что-то более удобоваримое, но они не слишком производительны (когда, к примеру, вызываешь их 1 000 000 и более раз). Ну и любопытно же…

Документация от IBM это вам не MSDN где все полочкам и с примерами… Тут вдумчивость нужна. И творческий подход.

В конечном итоге удалось установить, что в документации это называется Standard Time и представляет из себя количество микросекунд (да-да-да, именно микросекунд) от некоего начала эпохи. И никакой это не char[8], а вполне себе нормальный uint64.

А вот начало эпохи… Попробуйте угадать :-)

Hidden text

23/08/1928 12:03:06.314752

На всякий случай прописью — 23-е августа 1928-го года, 12 часов, 3 минуты, 6 секунд и еще 314752 микросекунды…

Как говорится, «что курили?»

И нет там никакой «проблемы 2038» как в остальном мире.

Hidden text

Зато есть проблема 08/06/2062 16:27:16.974591

Истину вам говорю — 8-го июня 2062-го года, ровно в 16 часов, 27 минут, 16 секунд и 974591 микросекунду Земля налетит на небесную ось…

Правда, если попытаться все-таки найти хоть какую-то логику, то внезапно становится понятно, что «эпоха» тут привязана не к началу или концу, а к середине. Ибо значение 0×8000000000000000 тут соответствует началу тысячелетия — 01/01/2000 00:00:00.000000

Ну и еще одна тонкость — для хранения собственно времени используется только 52 старших бита. А 12 младших — это т.н. «биты уникальности» которые используются (по задумке IBM) для создания уникальных меток с привязкой ко времени (с точностью до микросекунды). Т.е. «дергая» несколько раз это самое время даже в течении микросекунды, вы каждый раз будете получать уникальное значение, содержащее время и «сиквенс» — номер «внутри» микросекунды. В целом — красивое решение (если где-то потребуется).

К слову сказать, для matmdata определено несколько опций, возвращающих это самое время — локальное или UTC, с битами уникальности или без (всегда нули)

     #define _MDATA_CLOCK                 0x0000
     #define _MDATA_CLOCK_UTC             0x0004  /*              @A3A*/
     #define _MDATA_CLOCK_NOT_UNIQUE      0x0007  /*              @A6A*/
     #define _MDATA_CLOCK_UTC_NOT_UNIQUE  0x0008  /*              @A6A*/

(там есть еще набор опций, но ко времени они не относятся).

Теперь становится понятно что это такое и как из всего этого извлечь какую-то пользу. Например, перевести в привычный UNIXTIME.

Магическим числом тут будет 0×4A2FEC4C82000000 — значение _MI_Time, соответствующее началу UNIX эпохи — 01/01/1970 00:00:00.000000. А дальше просто:

#define IBM_EPOCH 0x4A2FEC4C82000000ULL
#define MKSECS    1000000ULL

  unsigned long long tod;
  unsigned           sec, usec;

  matmdata(&tod, _MDATA_CLOCK_UTC_NOT_UNIQUE);
  
  // приводим к UTC
  tod -= IBM_EPOCH;
  // убираем биты уникальности
  tod >>= 12;

  sec  = (unsigned)(tod / MKSECS);
  usec = (unsigned)(tod % MKSECS);

Получаем результат, идентичный тому, что возвращает gettimeofday () Но быстрее — 100 000 последовательных вызовов gettimeofday занимает 0.301682 сек, а тот же результат при помощи matmdata — 0.011416 сек в одних и тех же условиях.

Воистину — на AS/400 все не как у людей…

© Habrahabr.ru