При разработке высокопроизводительных механизмов, замере времени выполнения операций или создании сложных алгоритмов в 1С программисты часто используют встроенную функцию ТекущаяУниверсальнаяДатаВМиллисекундах(). Однако при детальном анализе результатов работы этой функции в цикле можно столкнуться с крайне странным поведением: время идет не плавно (по 1 миллисекунде), а совершает резкие скачки — «стыки» величиной ровно в 15–16 миллисекунд. На протяжении этого интервала функция возвращает абсолютно одинаковое значение.
В этой статье мы подробно разберем, куда «исчезают» эти 15 миллисекунд, проанализируем гипотезы разработчиков, выясним истинную причину такого поведения на уровне операционной системы и рассмотрим практические методы решения этой проблемы.
Рассмотрим классический эксперимент, который проводит автор темы. Мы создаем динамический массив и в быстром цикле заполняем его текущими значениями времени в миллисекундах:
Массив = Новый Массив;
Для А = 1 По 10000 Цикл
Массив.Добавить(ТекущаяУниверсальнаяДатаВМиллисекундах());
КонецЦикла;
При анализе полученного массива обнаруживается следующая картина: сначала идет длинная последовательность одинаковых чисел (например, 63 665 076 297 099), а затем происходит мгновенный скачок, и все последующие элементы заполняются значением 63 665 076 297 114. Разница между этими двумя значениями составляет ровно 15 миллисекунд.
Возникает логичный вопрос: почему быстродействующий процессор, который успевает за 15 миллисекунд выполнить тысячи итераций цикла, не получает промежуточных значений времени?
В процессе обсуждения разработчики выдвигают несколько предположений, которые на первый взгляд кажутся логичными, но при детальном анализе оказываются неверными.
Гипотеза 1: Время тратится на динамическое выделение памяти под массив.
Предполагается, что при достижении определенного лимита элементов платформа 1С приостанавливает выполнение кода для перевыделения памяти под массив в оперативной памяти (реаллокация), и именно этот процесс занимает 15 мс. Это легко опровергнуть. Если мы заранее выделим память под массив, инициализировав его фиксированным размером:
Массив = Новый Массив(10000);
Для А = 0 По 9999 Цикл
Массив[А] = ТекущаяУниверсальнаяДатаВМиллисекундах();
КонецЦикла;
Мы увидим, что характерные скачки по 15 миллисекунд все равно сохраняются. Выделение памяти в современных менеджерских кучах происходит значительно быстрее, и оно не может приводить к столь стабильным и фиксированным задержкам.
Гипотеза 2: Кэширование переменной самой платформой 1С.
Предполагается, что среда исполнения 1С оптимизирует вызовы и обновляет значение встроенной функции раз в 15 мс. Это также неверно. Платформа 1С не занимается искусственным кэшированием системного времени. При вызове функции ТекущаяУниверсальнаяДатаВМиллисекундах() происходит прямое обращение к API операционной системы.
Истинная причина кроется в архитектуре планировщика задач операционной системы Windows. Под капотом платформа 1С обращается к системной функции Windows API (обычно это GetSystemTimeAsFileTime).
Разберем этот механизм по шагам:
В ходе обсуждения авторы замечают странность: иногда при повторных тестах эти скачки по 15 мс внезапно пропадают, и функция начинает выдавать плавные значения с точностью до 1 миллисекунды. Это объясняется глобальным состоянием Windows Timer Resolution:
timeBeginPeriod(1).Если для вашей бизнес-логики или алгоритмов логирования критически важна стабильная точность времени до 1 миллисекунды и менее, стандартная функция ТекущаяУниверсальнаяДатаВМиллисекундах() в среде Windows без дополнительной настройки не гарантирует стабильного результата. Рассмотрим, как решить эту проблему.
Мы можем принудительно установить разрешение системного таймера Windows в 1 миллисекунду на время работы нашей сессии 1С. Для этого необходимо вызвать функцию timeBeginPeriod из системной библиотеки winmm.dll. Сделать это можно с помощью внешней компоненты или через технологию динамического вызова функций DLL (если позволяет окружение).
Примерный алгоритм работы выглядит следующим образом:
timeBeginPeriod(1). Это переводит системный таймер всей ОС в режим повышенной точности (1 мс).timeEndPeriod(1) для возврата системы к энергосберегающему режиму по умолчанию.Если вам требуется микросекундная точность для замера производительности участков кода (профайлинг), стандартный таймер времени не подойдет в принципе. В операционных системах Windows для этого реализован высокоточный счетчик времени QueryPerformanceCounter (QPC), который завязан на аппаратные такты процессора.
Для интеграции с 1С пишется простая внешняя компонента (например, на C++ или C#), которая предоставляет методы получения текущего значения этого счетчика. Схема работы компоненты на C# выглядит так:
// Пример логики внешней компоненты на C# для получения точного времени
long start = System.Diagnostics.Stopwatch.GetTimestamp();
// Выполнение измеряемого кода в 1С
long end = System.Diagnostics.Stopwatch.GetTimestamp();
double elapsedMicroseconds = (double)(end - start) * 1000000 / System.Diagnostics.Stopwatch.Frequency;
Этот подход полностью независим от настроек системного таймера Windows и дает чистую точность вплоть до микросекунд.
Стоит отметить, что если серверная или клиентская часть 1С работает под управлением ОС Linux, ситуация обстоит иначе. В Linux планировщик задач и системные таймеры обычно работают с гораздо более высокой базовой частотой обновления (зависит от конфигурации ядра, параметра HZ и поддержки режима Tickless Kernel), поэтому скачки времени там изначально менее выражены и обычно составляют не более 1–2 миллисекунд без дополнительных манипуляций.
Один из участников обсуждения упомянул, что планирует использовать миллисекунды для создания собственного Генератора Случайных Чисел (ГСЧ). Это классическая архитектурная ошибка, которой следует избегать:
ГенераторСлучайныхЧисел.Рассмотрим правильный пример инициализации и использования встроенного ГСЧ:
// Инициализация ГСЧ без параметров задействует аппаратную энтропию операционной системы
ГСЧ = Новый ГенераторСлучайныхЧисел();
Для Итератор = 1 По 10 Цикл
СлучайноеЧисло = ГСЧ.СлучайноеЧисло(1, 1000);
Сообщить("Случайное число: " + СлучайноеЧисло);
КонецЦикла;
Этот объект инициализируется на основе криптографических провайдеров безопасности операционной системы (CryptoAPI в Windows или устройства /dev/urandom в Linux), что гарантирует высокое качество энтропии и исключает дублирование или предсказуемость результатов, с которыми вы неизбежно столкнетесь при попытке написать собственный велосипед на базе системного времени.