Standard Time как его видит IBM
Есть многое в природе, друг Горацио, Что и не снилось нашим мудрецам.© Вильям Шекспир
Более 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 все не как у людей…