Как работает выражение Date.Now < Date.Now?

Рейтинг: 8Ответов: 2Опубликовано: 07.08.2025

Описание

Во время обсуждения с коллегами неожиданно возникла идея такого "почти бесконечного" цикла:

Console.WriteLine("Begin");
while (DateTime.Now < DateTime.Now)
{
    Console.WriteLine("Loop cycle");
}
Console.WriteLine("End");

Интересно, что результат у всех оказался разным:

Да и разные ЯП по-разному реагируют. К примеру JS вообще ультанул:

console.log("Begin");
while (Date.now() < Date.now()) {
    console.log("Loop cycle");
}
console.log("End");

Вопрос

Как вообще работает выражение Date.Now < Date.Now? На что можно опираться в языках программирования, чтобы понять, каким будет результат?

Ответы

▲ 13Принят

Ни на что нельзя опираться. Выражение DateTime.Now < DateTime.Now задействует два побочных эффекта, а это обычно кончается плохо.

Каждый вызов DateTime.Now опирается на побочный эффект – получает текущее время. С одной стороны, мы, как будто, ничего не меняем. С другой стороны чтение меняющегося значения тоже побочный эффект.

Иногда язык определяет порядок вычисления подвыражений. Если это так, то левая сторона, скорее всего, будет вычислена первой. Если порядок не определён, то первой может вычисляться любая сторона неравенства.

Если язык быстрый, а разрешение используемого таймера низкое, то в большинстве случаев левая и правая сторона вернут одинаковые значения. Скорее всего это происходит в примере на JavaScript. В нём разрешение таймера 1ms, а интерпретатор успевает за миллисекунду исполнить десятки тысяч инструкций (Термин "быстрый" означает что язык успевает исполнить много инструкций за один тик таймера).

Если язык медленный, а таймер буквально наносекундный, то, чаще всего, будет неравенство. В C#, насколько я знаю, порядок вычисления определён: сперва вычислится левое подвыражение, потом правое. Правда в документации сказано что разрешение таймера от 0.5ms до 15ms. То есть получить неравенство больше одного раза нельзя. Но если вы запустите программу, которая печатает DateTime.Now.Ticks в цикле, вы увидите что отметки времени выдаются разные каждые 100μs. То есть таймер обновляется 10 миллионов раз в секунду. А вызов DateTime.Now на моём железе делается 30 миллионов раз в секунду. Это вполне сравнимые величины, которые могут, если вам повезёт, довольно долго выдавать неравенство для последовательных вызовов. В нашей терминологии C# оказался "медленным" языком.

В C подобное выражение вообще может оказаться неопределённым поведением, всё-таки два побочных эффекта в одном выражении, порядок вычисления которого не определён языком и может меняться в зависимости от фазы Луны, например.

▲ 3

Добавлю "натурных испытаний" на Питоне. Миллион раз прогоним метод time.time, который обладает слабым разрешением, и столько же раз метод time.perf_counter с разрешением получше, и поэтому рекомендованный для замеров времени в Питоне:

import time
import pandas as pd
import seaborn as sns

def test_func(f):
    cnt = 0
    while f() < f():
        cnt += 1
    return cnt

n = 1_000_000
df = pd.DataFrame(
{
    'func': [x for x in ('perf_counter', 'time') for _ in range(n)],
    'data': [test_func(f) for f in (time.perf_counter, time.time) for _ in range(n)]
})
sns.boxenplot(df, x='func', y='data')
plt.xlabel('метод')
plt.ylabel('несовпадения подряд')
plt.title(f'Серия из {n:,d} проверок f()<f()')

введите сюда описание изображения

Как видно, time.time практически всегда совпадает при сравнении, а вот у time.perf_counter бывают довольно большие случайные промежутки не совпадения периодически.