
Показанным ниже кодом вы можете проверить на високосность год в интервале 0 ≤ y ≤ 102499 всего примерно тремя командами CPU:
bool is_leap_year_fast(uint32_t y) {
return ((y * 1073750999) & 3221352463) <= 126976;
}
Как это работает? Ответ на удивление сложен. В статье я объясню процесс; в основном он связан с забавным битовым жонглированием. В конце мы обсудим применение этого кода на практике.
Вот, как обычно реализуется проверка на високосность:
bool is_leap_year(uint32_t y) {
if ((y % 4) != 0) return false;
if ((y % 100) != 0) return true;
if ((y % 400) == 0) return true;
return false;
}
Мы пользуемся пролептическим григорианским календарём, расширяющим григорианский календарь с момента начала его использования в 1582 году до нулевого года. Благодаря этому нам не нужно как-то по-другому обрабатывать даты до 1582 года. Для простоты мы не будем учитывать отрицательные года и используем беззнаковые величины.
Оптимизируем стандартное решение
Для начала реализуем простые трюки для ускорения, чтобы получить точку отсчёта. Я не знаю, кто конкретно их придумал — вероятно, эти трюки многократно изобретались заново.
Можно заменить (y % 100) != 0
на (y % 25) != 0
: мы уже знаем, что y
кратен 22, поэтому если он кратен 52, то кратен и 22 ⋅ 52 = 100. Аналогично, мы можем заменить (y % 400) == 0
на (y % 16) == 0
: мы уже знаем, что y кратен 52, поэтому если он также кратен 24, то кратен и 52 ⋅ 24 = 400.
bool is_leap_year1(uint32_t y) {
if ((y % 4) != 0) return false;
if ((y % 25) != 0) return true;
if ((y % 16) == 0) return true;
return false;
}
Это полезно, потому что теперь мы можем заменить деление с остатком на 4 и 16 побитовым маскированием. Есть и ещё один трюк, хорошо известный разработчикам компиляторов, позволяющий избавиться от деления с остатком на 25. Скомпилировав (x % 25) != 0
при помощи gcc и выполнив трансляцию обратно на C, мы получим x * 3264175145 > 171798691
. Так как умножение обычно имеет задержку 3 такта, а деление с остатком — не менее 20 тактов, это солидное улучшение. Я объясню в общих чертах, как это работает; подробности можно найти в
В статье Faster remainder by direct computation: Applications to compilers and software libraries Дэниела Лемайра, Оуэна Кейсера и Натана Курца про уменьшение на константу делителя при делении с остатком;
В Euclidean affine functions and their application to calendar algorithms Кассио Нери и Лоренца Шнайдера про календарные вычисления
В Identifying leap years Дэвида Тёрнера о проверке високосности (в том числе с формальными доказательствами!).
Откуда берутся эти магические числа? У нас есть
232 ⋅ 19/25 = 3264175144,96 (ровно).
То есть умножив на 3264175145, мы приблизительно вычисляем дробную часть умножения на (19/25). Если это было точное умножение, мы получим для кратных 25 чисел дробную часть, равную нулю. Однако если мы умножим на число, которое больше на 0,04, то погрешность может быть до 0,04 ⋅ (232 - 1) = 171798691,8; отсюда и берётся второе магическое число.
Этот трюк не так хорошо работает для x % 100
, где нам нужна ещё одна fixup-команда, так что уменьшение с y % 100
до y % 25
было очень полезным.
Теперь наша проверка на високосность выглядит так:
bool is_leap_year2(uint32_t y) {
if ((y & 3) != 0) return false;
if (y * 3264175145u > 171798691u) return true;
if ((y & 15) == 0) return true;
return false;
}
Стоит отметить, что современные компиляторы наподобие gcc и clang создают из is_leap_year1
что-то наподобие is_leap_year2
, так что делать это в исходниках на C нет особого смысла, но в других языках это может оказаться полезным.
Обычно такой код компилируется в ассемблерный код с ветвлением. На практике вводимые данные обычно достаточно предсказуемы, поэтому это необязательно плохо. Если мы хотим избежать снижения производительности из-за ошибок прогнозирования ветвления, то ценой замедления наилучшего случая можем немного изменить структуру и получить код без ветвления (и одновременно хорошего кандидата для соревнований в гольф-кодинге):
bool is_leap_year3(uint32_t y) {
return !(y & ((y % 25) ? 3 : 15));
}
Если вы хотите узнать о других способах ускорения календарных вычислений, то изучите Optimizing with Novel Calendrical Algorithms Джейкоба Пратта.
Находим решение с битовым жонглированием
Можно ли усовершенствовать вычисление високосности, отказавшись от корректности всех входных данных? В конце концов, обычно нас не волнует, будет ли високосным 3584536493 год; и в самом деле, Python, C# и Go поддерживают года с 0 (или 1) до 9999 (в котором смещение относительно времён года уже будет больше четырёх дней). Я подумал, что если существует более короткое решение, то оно бы выглядело как какое-то странное хэширование с магическими константами, поэтому решил попробовать малые формы и подобрать константы перебором. Вид (y * f) <= t
кажется полезным, но пока недостаточно мощным. Одним из моих кандидатов стало добавление маски: ((y * f) & m) <= t
. Теперь у нас есть для угадывания 96 бита, одним лишь брутфорсом это не решить. Давайте воспользуемся z3 — солвером, поддерживающим ограничения битовых векторов, что идеально нам подходит:
import z3
BITS = 32
f, m, t, y = z3.BitVecs('f m t y', BITS)
def target(y):
return z3.And((y & 3) == 0, z3.Or(z3.URem(y, 25) != 0, (y & 15) == 0))
def candidate(x):
return z3.ULE((x * f) & m, t)
solver = z3.Solver()
solver.add(z3.ForAll(y, z3.Implies(z3.ULE(y, 400),
candidate(y) == target(y))))
if solver.check() == z3.sat:
print(f'found solution: {solver.model()}')
else:
print('no solution found')
За несколько секунд он обнаружил константы, позволяющие получать корректный результат для нетривиального интервала значений годов. Расширяя этот интервал, я в конечном итоге примерно за полчаса вычислений обнаружил константы, дающие корректный результат с года 0 по год 102499, и доказал, что для 32 битов это оптимум:
bool is_leap_year_fast(uint32_t y) {
const uint32_t f = 1073750999u;
const uint32_t m = 3221352463u;
const uint32_t t = 126976u;
return ((y * f) & m) <= t;
}
Объяснение
Как это работает? Кажется неожиданным, почти волшебным, что мы можем уместить все эти вычисления всего в три команды. Однако описанное выше даёт нам основные инструменты для понимания этого.
Вот наши константы в двоичном виде; четыре релевантных интервала битов обозначены буквами:

Давайте сначала разберёмся, что делает маскирование при помощи m и сравнение с t для произведения p := y ⋅ f. В блоке A биты t равны 0, а значит, если какой-то из битов в A ненулевой в p, результатом будет false. В противном случае релевантным становится блок B. Здесь все биты в t равны 1, поэтому результат равен true, если какой-то из битов B имеет в p ненулевое значение. В противном случае для блока C мы требуем, чтобы все биты в p были нулевыми. Благодаря этому куча сравнений интервалов битов объединена в единственный <=
.
То есть мы можем переписать is_leap_year_fast
следующим образом:
bool is_leap_year_fast2(uint32_t y) {
uint32_t p = y * 1073750999u;
const uint32_t A = 0b11000000000000000000000000000000;
const uint32_t B = 0b00000000000000011111000000000000;
const uint32_t C = 0b00000000000000000000000000001111;
if ((p & A) != 0) return false;
if ((p & B) != B) return true;
if ((p & C) == 0) return true;
return false;
}
Это подозрительно похоже на is_leap_year2
! И в самом деле, три условия имеют точно такое же предназначение. Мы покажем, что
(p & A) != 0
срабатывает, когда(y % 4) != 0
;(p & B) != B
срабатывает, когда(y % 100) != 0
;(p & C) == 0
срабатывает, когда(y % 16) == 0
(и, следовательно,(y % 400) == 0
, потому что мы уже знаем, что y кратно 25).
Два простых случая: (1) и (3)
(1): Бит 1 блока A в f воссоздаёт два младших бита y в p в блоке A. Их нельзя спутать с результатом умножения на биты в D: максимальное значение, которое мы можем получить — это 102499 ⋅ (f & D) = 940428325, состоящее всего из 30 битов. Таким образом, проверка A на нулевое значение в p эквивалентно проверке, равен ли нулю остаток от деления y на 4.
(3): Проверка того, что ни один из младших 4 битов не задан в p — это проверка того, равен ли нулю остаток от деления p на 16. Однако на самом деле мы хотим проверить y. Это не проблема: достаточно взглянуть только на младшие 4 бита f, а f здесь 11112 = 15. Умножение на 15 = 3 ⋅ 5 не добавляет нового делителя 2, поэтому состояние делимости на 16 не меняется.
Любопытный случай: (2)
Теперь давайте попробуем выяснить, для каких чисел p & B ≠ B. Для этого бит 1 в f & A не играет роли, поэтому рассмотрим биты в f & D. Они равны 100011110101112 = 9175. Давайте проверим, какие числа пройдут тест.
>>> B = 0b00000000000000011111000000000000
>>> s = [y for y in range(5000) if ((y * 9175) & B) == B]
>>> for i in range(0, len(s), 16): print(*(f'{n:4d}' for n in s[i:i+16]))
14 57 71 100 114 157 171 200 214 257 271 300 314 357 371 400
414 457 471 500 514 557 571 600 614 657 671 700 714 757 771 800
814 857 871 900 914 957 971 1000 1014 1057 1071 1100 1114 1157 1171 1200
1214 1257 1271 1300 1314 1357 1371 1400 1414 1457 1471 1500 1514 1557 1571 1600
1614 1657 1671 1700 1714 1757 1771 1800 1814 1857 1871 1900 1914 1957 1971 2000
2014 2057 2071 2100 2114 2157 2171 2200 2214 2257 2271 2300 2314 2357 2371 2400
2414 2457 2471 2500 2514 2557 2571 2600 2614 2657 2671 2700 2714 2757 2771 2800
2814 2857 2871 2900 2914 2957 2971 3000 3014 3057 3071 3100 3114 3157 3171 3200
3214 3257 3271 3300 3314 3357 3371 3400 3414 3457 3471 3500 3514 3557 3571 3600
3614 3657 3671 3700 3714 3757 3771 3800 3814 3857 3871 3900 3914 3957 3971 4000
4014 4057 4071 4100 4114 4157 4200 4214 4257 4300 4314 4357 4400 4414 4457 4500
4514 4557 4600 4614 4657 4700 4714 4757 4800 4814 4857 4900 4914 4957
Здесь есть числа, кратные 100, как мы и хотели, но и куча других чисел. Это не проблема, если ни одно из них не окажется кратным 4, потому что мы уже отфильтровали их на предыдущем этапе. Кроме того, отсутствует 0, но это тоже не проблема, потому что 0 также кратен 400.
Давайте попробуем разобраться в паттерне. На первый взгляд, он выглядит очень простым: у нас есть *14, *57, *71, и *00. Однако начиная с 4171 числа *71 пропадают (вы заметили?). Позже появляются новые паттерны. Давайте проанализируем это. Результат работы программы на Python
def test(y):
B = 126976
return ((y * 9175) & B) == B
active = set()
for y in range(120000):
r = y % 100
if test(y):
if r not in active:
print(f'{y:6}: started *{r:02}')
active.add(r)
else:
if r in active:
print(f'{y:6}: stopped *{r:02}')
active.remove(r)
будет таким:
14: started *14
57: started *57
71: started *71
100: started *00
4171: stopped *71
32843: started *43
36914: stopped *14
65586: started *86
69657: stopped *57
98329: started *29
102500: stopped *00
То есть начиная с 102500 мы больше не отлавливаем числа, кратные 100, и именно поэтому 102499 — последнее число, для которого is_leap_year_fast
возвращает корректный результат. Также мы видим, что ниже него нет ни одного числа, кратного 4, за исключением кратных 100 (удобно, что мы можем проверить это, зная только два последних десятичных разряда). Если мы доверимся этой проверке перебором, то доказательство условия (2) на этом завершается; но давайте продолжим, чтобы лучше понять, почему получаются именно такие числа.
Для начала давайте разберёмся, почему мы получаем значения, кратные 100. Делитель 9175 близок к кратному 1/100 в 17-битном представлении с фиксированной запятой:
217 ⋅ 7/100 = 9175,04 (ровно).
Умножив число, кратное 100, на 9175,04, мы получим целое число (кратное 7) в битах 17 и выше, а биты ниже 17 будут нулевыми. Пример:
9175,04 ⋅ 500 = 100011000000000000000002, где 1000112 = 35 = 5 ⋅ 7.
Умножив число, кратное 100, на 9175, мы получим немного меньше:
9175 ⋅ 500 = 100011000000000000000002 − 500 ⋅ 0,04 = 100010111111111111011002.
В общем случае, вычитание небольшого значения из числа, заканчивающегося на кучу нулей, создаёт число, заканчивающееся на кучу единиц, за исключением самого конца. Здесь мы проверяем 5 битов в B. Для y, кратного 100, они все гарантированно будут единицами, если только накопленная ошибка не достигнет конца B, что произойдёт только после y = 212 / 0,04 = 102400, что нам подходит.
Откуда же берутся другие числа наподобие 14, 57 и 71? Давайте взглянем на это под другим углом. У нас есть 9175 = 217 ⋅ 0,06999969482421875 (ровно) и B = 217 ⋅ 0,96875, поэтому
p & B= B
⇔{y ⋅ 0,06999969482421875}≥ 0,96875, где {x} — дробная часть x
⇔6,999969482421875y mod 100≥ 96,875
Это ещё один способ понять, почему принимаются числа, кратные 100: для них 7y mod 100 равно 0, поэтому 6,999969482421875y mod 100 оказывается чуть меньше 100, и падает ниже 96,875 только после y = (100 − 96,875) / (7 − 6,999969482421875) = 102400.
Чтобы понять другие числа, встречающиеся в нашей последовательности, давайте сначала рассмотрим, какими будут решения, если бы в этом неравенстве было ровно 7:
7y mod 100≥ 96,875
⇔ 7y mod 100∈ {97, 98, 99}.
Чтобы найти решения этого, нам сначала потребуется число, обратное по модулю 7 modulo 100, то есть число x такое, что 7x mod 100 = 1. Мы можем вычислить его при помощи расширенного алгоритма Евклида или просто при помощи какого-нибудь онлайн-калькулятора, который сообщит нам, что результат равен 43. Тогда решениями будут 43 ⋅ 97 (mod 100), 43 ⋅ 98 (mod 100) и 43 ⋅ 99 (mod 100), то есть, соответственно, 71, 14 и 57 (mod 100). Это объясняет, почему мы сначала встречаем числа вида *14, *57 и *71. Это также объясняет, почему мы перестаём встречать, например, *71 после 4071: хотя 7 ⋅ 4171 = 29197, мы получаем 6,999969482421875 ⋅ 4171 = 29196,872711181640625, что (modulo 100) меньше, чем 96,875. Аналогично, мы встречаем 32843, потому что накопившаяся погрешность (7 − 6,999969482421875) ⋅ 32843 = 1,002288818359375 превышает единицу. Приложив ещё немного усилий, мы можем вручную воссоздать результат приведённой выше программы на Python и убедиться, что ни одно из этих чисел не кратно 4.
Расширение до других битовых ширин
Теперь, когда мы знаем, как работает этот трюк, можно попробовать подобрать параметры для других битовых ширин. Изменятся местоположение блока B и числитель знаменателя 100 в f & D.
uint64_t score(uint64_t f, uint64_t m, uint64_t t) {
for (uint64_t y = 0; ; y++)
if ((((y * f) & m) <= t) != is_leap_year(y))
return y;
}
int main() {
uint64_t best_score = 0;
for (int k = 0; k < BITS; k++) {
for (int k2 = 0; k2 < k; k2++) {
uint64_t t = (1ULL << k) - (1ULL << k2);
uint64_t m = (0b11ULL << (BITS - 2)) | t | 0b1111;
for (int n = 0; n < 100; n++) {
uint64_t f = (0b01ULL << (BITS - 2)) | (((1ULL << k) * n) / 100);
uint64_t new_score = score(f, m, t);
if (new_score > best_score) {
printf("%llu %llu %llu: %llu (%d %d %d)\n",
f, m, t, new_score, k, k - k2, n);
best_score = new_score;
}
}
}
}
return 0;
}
При BITS = 64
мы примерно за 7 минут находим f = 4611686019114582671, m = 13835058121854156815, t = 66571993088, которые корректны вплоть до y = 5965232499. Это здорово, потому что 5965232499 > 232, а значит, таким вариантом кода можно протестировать любой 32-битный год.
Какого максимального года мы можем достичь с 64 битами? Возможно, есть другие константы, которые работают ещё лучше? Я не могу сходу найти способ доказать это, поэтому попросил заняться этим других людей, создав пост о задаче в Code Golf StackExchange. И спустя всего один час пользователь ovs опубликовал очень хороший результат, а два дня спустя пользователь Exalted Toast выложил доказательство того, что 5965232499 и в самом деле — наилучший возможный интервал для 64 битов, тоже воспользовавшись солвером z3.
Бенчмарк
Получить чёткие бенчмарки в этом случае сложно, потому что функция выполняется очень малое количество; более того, время исполнения версий с ветвлением зависит от паттернов ввода. Попробуем два крайних случая: всегда 2025 год и полностью случайные годы. Ниже приведены результаты бенчмарка на i7-8700K (Coffee Lake, 4.7 GHz), скомпилированного с g++ -O3 -fno-tree-vectorize
:
2025 (нс) |
случайный (нс) |
|
---|---|---|
|
0.65 |
2.61 |
|
0.65 |
2.75 |
|
0.67 |
0.88 |
|
0.69 |
0.69 |
Вот некоторые из странностей:
При случайных значениях
is_leap_year2
чуть медленнееis_leap_year
. Это неожиданно, потому что дляy % 100
требуется на одну команду больше, чем трюку вis_leap_year2
.is_leap_year3
чуть медленнее для случайных данных, чем для фиксированного значения. Это неожиданно, потому что функция не выполняет никакого ветвления.
У меня нет никаких других объяснений этому, кроме как то, что создание бенчмарков — сложная задача.
Для случайных данных новая функция is_leap_year_fast
в 3,8 раза быстрее, чем стандартная реализация, а для полностью прогнозируемого ввода она примерно на 6% медленнее. В целом, результаты кажутся вполне стабильными.
Заключение
Стоит ли оно того? Нужно ли нам заменять, например, реализацию datetime CPython на этот трюк? Ответ зависит от обстоятельств. На практике, чаще всего будут запрашивать проверку текущего года, или, по крайней мере, запросы будут достаточно предсказуемы; в таком случае особого преимущества мы не получим. Чтобы изменения оправдали себя, в идеале нам нужен бенчмарк с реалистичными данными, который использует в качестве подпрограммы проверку года на високосность, а не простой микробенчмарк. С удовольствием услышал бы о результатах подобных измерений!
Комментарии (58)
MaxAkaAltmer
18.05.2025 06:39А разве високосный год, не каждый четвертый?
Такого выражения вроде бы должно быть достаточно: !(y&3)ПС. Да там оказывается еще другие периоды учитываются. Не знал )
perfect_genius
18.05.2025 06:39Посмотрите в поисковике "Заблуждения программистов о времени". На Хабре даже две такие статьи.
Поэтому после таких сложностей выглядит не очень убедительно, когда по ТВ очередной "гениальный" ребёнок может назвать день недели по любой дате.
pnmv
18.05.2025 06:39возможно, я не уловил какую-нибудь иронию или ещё что-либо, поэтому спрошу прямо: те два предложения, ниже заголовка "Расширение до других битовых ширин", специально оставлены без перевода?
(вообще, конечно, после таких выкладок, вывод слегка обескураживает)
PatientZero Автор
18.05.2025 06:39Нет, это мой недосмотр, сейчас исправлю, спасибо
kimstik0
18.05.2025 06:39Люблю такие штуки. И спасибо за Z3. очень интересует. второй раз вижу как она реально применяется для оптимизации.
Oeaoo
18.05.2025 06:39Интересно, но зачем.
perfect_genius
18.05.2025 06:39Для тех, кто не знает про "Заблуждения программистов о времени" и думает всё сделать самому.
zakker
18.05.2025 06:39Сначала я такой: О! Крутая оптимизация. Заберу в свою коллекцию.
А потом задумался. А в какой задаче требуется максимально быстро проверять год на високосность? Думал думал и так и не придумал. Единственное, что приходит в голову - преобразование даты в секунды при обработке каких-то массивов данных. Но всё равно мысль буксует, зачем это надо делать быстро и даже если очень надо, не быстрее ли сделать какой-то lookup в табличку с нужным диапазоном годов и сразу получать готовые вычисления.
Но в целом я одобряю подобные извращения над битовой арифметикой. Как разминка для ума, и, иногда оно бывает очень полезным с практической точки зрения
Serge3leo
18.05.2025 06:39А в какой задаче требуется максимально быстро проверять год на високосность? Думал думал и так и не придумал. Единственное, что приходит в голову - преобразование даты в секунды при обработке каких-то массивов данных.
Честно говоря. Преобразование григорианской даты в/из юлианского дня или в/из числа атомных секунд от заданой эпохи выполняется почти линейной формулой (таблица нужна только для високосных секунд).
А предикат високосного года, вроде как, нужен только для входного контроля дат февраля или расчёта номера дня в году (номера недели в году).
Так что, само по себе это просто красивая конструкция. Возможно, её аналог можно где-то ещё применить, кто знает?
cupraer
18.05.2025 06:39Добавить
N
секунд (минут, дней) к дате — это не такая уж и редкая задача.zakker
18.05.2025 06:39Дело не в редкости задачи, а в том, сколько раз в секунду эту задачу приходится выполнять. Конкретно эта задача - определение високосности года - если и требуется где-то, то не чаще одного раза за кадр, что явно недостаточно, чтобы заморачиваться ради сотни тактов.
speshuric
18.05.2025 06:39Если это в запросе СУБД на миллиард строк, то, наверное, может стать важным.
Другой вопрос, что возникает много оговорок. Во-первых, даже для манипуляций с датами в чистом виде эта функция не очень нужна. Во-вторых, в классической OLTP СУБД будет гораздо больше накладных расходов на доступ к полю (в колоночных аналитических может и есть какой-то смысл).
cupraer
18.05.2025 06:39Вы мыслите как прикладник, попробуйте мыслить как создатель вспомогательного кода. Представьте себе: вы реализовываете библиотеку общего назначения, в которой есть функция
date_add(origin, seconds)
.Пользователь вашей библиотеки реализует на её базе крон.
Пользователь крона просит ему показать дату, когда будет миллионое срабатывание (для метрик своих каких-нибудь).
Вот простейший сценарий, когда функция общего назначения будет вызвана миллион раз в секунду.
unreal_undead2
18.05.2025 06:39Пользователь крона просит ему показать дату, когда будет миллионое срабатывание
И вы считаете её перебором?
cupraer
18.05.2025 06:39А вы знаете хитрый метод определения этой даты? Поделитесь, пожалуйста, мне очень бы пригодилось в двух моих библиотеках.
unreal_undead2
18.05.2025 06:39Если событие срабатывает раз в день/неделю/месяц, не вижу проблем вычислить дату n-ого срабатывания аналитически. Но не помню, насколько сложные правила можно задать в кроне - можете приветси что-нибудь нетривиальное?
cupraer
18.05.2025 06:39Событие «каждый четверг» в феврале 2024 отрабатывало 5 раз.
Событие «каждую секунду последнего часа» отрабатывало 61 раз в ночь с 30 июня на 1 июля 1997 года.
unreal_undead2
18.05.2025 06:39Ну, банальные високосные годы и leap seconds - согласен, что надо подумать, но уверен, что аналитическая формула и там и там есть. Вывести автоматическую генерацию аналитической формулы из конфига крона - да, интересная задачка )
cupraer
18.05.2025 06:39Формулы чего именно?
cupraer
18.05.2025 06:39Високосные секунды добавляются «ad hoc», высочайшим распоряжением астрономических чиновников. Мы не знаем, когда будет добавлена следующая, поэтому задача с учетом високосных секунд для будущего — решения в принципе не имеет, даже перебором.
Если про них забыть, и просто перезапускать расчет, например, каждую полночь, то я практически убеждён (как человек, который очень подробно исследовал всю эту астрономию при написании вышеупомяных библиотек): даже если аналитическая формула и существует, человечеству она не известна, а если бы была известна — едва ли была бы быстрее индукции. Следующее срабатывание я считать умею.
unreal_undead2
18.05.2025 06:39Да, високосные секунды тогда просто игнорим. Вычислить миллионный четверг в терминах обычного юниксового таймстампа тривиально, конверсия таймстампа в год/месяц/день - есть в библиотеках, явно без перебора.
cupraer
18.05.2025 06:39Тривиально, конечно; вот я вам и код, которым 99% библиотек пользуются, подогнал: https://github.com/coreutils/gnulib/blob/master/lib/parse-datetime.y#L1747-L2383
Явно без перебора. Или с ним всё-таки?
Только есть нюанс: этот код не умеет работать с не-ISO календарями, а для библиотеки, написанной в XXI веке — это не вариант.
speshuric
18.05.2025 06:39"Добавить дней/секунд к дате" можно выполнять в удобном для хранения формате, храня смещение относительно какой-то точки (собственно, отсюда же системный формат unix и растёт). А преобразование в человеческую дату и обратно требует другой задачи - сколько високосных лет (29.02) прошло до этого момента. И "добавить N месяцев/лет" проще делать конвертацией сначала в структуру годы-месяцы-дни, потому что всё равно 29.02 отдельно обрабатывать.
Serge3leo
18.05.2025 06:39Строго говоря, решение задачи "... сколько високосных лет (29.02) прошло..." не зависит от решения задачи проверки года на високосность.
И "добавить N месяцев/лет" проще делать конвертацией сначала в структуру годы-месяцы-дни, потому что всё равно 29.02 отдельно обрабатывать.
Не очень понятно. Проще и надёжнее преобразовать в юлианские дни или секунды, потом добавить и преобразовать назад, так все и делают (chrono::time_point/chrono::duration или datetime.datetime/datetime.timedelta). А в этом случае, 29.02 не требует отдельной обработки, там просто линейные формулы.
Можно ли этот обощённый линейный алгоритм "обогнать" для случая фиксированного и/или не очень большого интервала? Вопрос непонятный (ИМХО, сомнительный и, вероятно, отрицательный).
cupraer
18.05.2025 06:39для хранения формате, храня
Где храня? Отвлекитесь от джейсоноукладки, представьте себе, что вы реализовываете системную функцию
date_add(origin, seconds)
в стандартной библиотеке какого-нибудь раста или го.Которую пользователь вашей библиотеки может вызвать в цикле, чтобы добраться до даты в следующем году по заковыристым бизнес-правилам. Это миллион вызовов.
antonkrechetov
18.05.2025 06:39Которую пользователь вашей библиотеки может вызвать
... после 102499 года.
Hlad
18.05.2025 06:39Быстро - не надо, скорее всего. А вот с ситуацией "уложить проверку високосности в минимальное количество байт" я сталкивался.
svistkovr
18.05.2025 06:39Можно заменить
(y % 100) != 0
на(y % 25) != 0
(y % 400) == 0
на(y % 16) == 0
непонятно откуда вы берёте такое сокращение. они дают разный результат
например
150 % 100 = 50 возвращает true так как не равно 0
150 % 25 = 0 возвращает false так как равно 0VPryadchenko
18.05.2025 06:39Так проверка на 100 делается тогда, когда y делится на 4. 150 на 4 не делится.
Serge3leo
18.05.2025 06:39Как бы, предусловия и взаимную простоту делителей стоит учитывать.
bool is_leap_year1(uint32_t y) { if ((y % 4) != 0) return false; assert(0 == y%4); if ((y % 25) != 0) return true; assert(0 == y%100); // Поскольку 4 и 25 взаимно просты if ((y % 16) == 0) return true; assert(0 != y%400); // Поскольку 16 и 25 взаимно просты return false; }
Kelbon
18.05.2025 06:39Бенчмарк показывает, что варианты не особо различаются по производительности (или "fast" версия даже проигрывает)
https://quick-bench.com/q/aH7Tc4TN4000UXQSitdj9aososIjohnfound
18.05.2025 06:39Чтобы точно сказать, надо смотреть на компилированный код. Вполне возможно, что другие операции вокруг целевой функции выполняются намного дольше, или компилятор что-то напутал. В идеальном случае я бы просто написал все на ассемблере, тогда сразу становится ясным кто быстрее и почему.
Kelbon
18.05.2025 06:39Нет, ассемблер как раз не даёт никакой ясности что будет быстрее. Больше кода не значит что он будет медленнее
johnfound
18.05.2025 06:39Конечно что не значит. Но я такое и не утверждал. Надо же посмотреть что именно выполняется. Вот угадайте в C что именно выполняется на процессоре.
wmlab
18.05.2025 06:39Интересная статья. Подобные трюки я встречал раньше. Например, при вычислении расстояния Хэмминга, когда нужно посчитать количество отличающихся битов двух 64-битных векторов. Делать циклический сдвиг 63 раза и считывать младший бит? Долго.
__declspec(dllexport) int ph_hamming_distance(const ulong64 hash1, const ulong64 hash2) { ulong64 x = hash1 ^ hash2; const ulong64 m1 = 0x5555555555555555ULL; const ulong64 m2 = 0x3333333333333333ULL; const ulong64 h01 = 0x0101010101010101ULL; const ulong64 m4 = 0x0f0f0f0f0f0f0f0fULL; x -= (x >> 1) & m1; x = (x & m2) + ((x >> 2) & m2); x = (x + (x >> 4)) & m4; return int((x * h01) >> 56); }
Потом в SSE4.2 (2008) появилась команда POPCNT и надобность в подобном цирке отпала.
Еще больше трюков можно найти в исходном коде Quake. На что только не шли, чтобы избежать долгих вычислений на старых CPU! Вот так считали обратный квадратный корень:
float Q_rsqrt(float number) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = *(long*)&y; i = 0x5f3759df - (i >> 1); y = *(float*)&i; y = y * (threehalfs - (x2 * y * y)); return y; }
На хабре про это уже писали: «Магическая константа» 0x5f3759df / Хабр
checkpoint
18.05.2025 06:39Сижу, перелистываю книгу "The Practice of Programming" от B. Kernighan и R.Pike, и вижу следующее:
И тут Ваша статья. Совпадение ? Не думаю. ;)
D_Dementy
18.05.2025 06:39sub isleap { my ($year) = @_; return 1 if ( $year % 400 == 0 ); # 400's are leap return 0 if ( $year % 100 == 0 ); # Other centuries are not return 1 if ( $year % 4 == 0 ); # All other 4's are leap return 0; # Everything else is not }
Perl
codecity
18.05.2025 06:39Вариант DeepSeek:
#include <stdint.h> int is_leap_year(int year) { // Оптимизация year % 4 через битовую операцию if (year & 3) return 0; // Не делится на 4 → не високосный // Быстрый расчёт year % 100 через "магическую константу" uint64_t tmp = (uint64_t)year * 1374389535ULL; int mod100 = year - ((int)(tmp >> 37) * 100); if (mod100 != 0) return 1; // Делится на 4, но не на 100 → високосный // Проверка year % 400 (аналогично через константу) tmp = (uint64_t)year * 1374389535ULL; // Та же константа работает для /400 int mod400 = year - ((int)(tmp >> 37) * 400); return mod400 == 0; }
или
int is_leap_year(int y) { return !(y & 3) && (y - ((int)((uint64_t)y * 1374389535ULL >> 37) * 100) || !(y - ((int)((uint64_t)y * 1374389535ULL >> 37) * 400)); }
Serge3leo
18.05.2025 06:39Антиресно, а DeepSeek сам лажанулся и выдал ответ для знаковых
int year, mod100, mod400
? Или это уже Ваша ошибка с переводом из корректных беззнаковыхuint64_t year, mod100, mod400 в некорректные знаковые?
codecity
18.05.2025 06:39Или это уже Ваша ошибка с переводом
Я тут ничего не переводил. Вообще такие задачи лучше давать o3, но до конца месяца у меня доступа не будет.
Serge3leo
18.05.2025 06:39Понятно, и не переводили, и не проверяли. Бывает.
Только непонятно, Deepseek, выдал оба варианта кода, и 18 строк, и 1 строку одновременно?codecity
18.05.2025 06:39Только непонятно, Deepseek, выдал оба варианта кода, и 18 строк, и 1 строку одновременно?
Я попросил записать в виде одной строки.
Не смотрел, т.к. задача не особо актуальна. Но чисто для дополнения - решил добавить в комментах.
Интересно было бы посмотреть вариант o3.
Grigory_T
18.05.2025 06:39просто для интереса сделал:
до 9999 года нашел еще такую формулу, 22 бита.
static inline bool is_leap22(uint32_t y)
{
return ((y * 1648277u) & 0x3FFFFFu) <= 5103u;
}
" Берём 400-летний период Григорианского календаря, формулируем условие «((y × M) & MASK) ≤ T ⇔ високосный» как задачу выполнимости для 400 остатков. Далее отдаём её SMT-решателю Z3 с критерием «минимизировать сумму 22-битных констант M, MASK, T». Z3 за доли секунды подбирает первую модель, потом скрипт автоматически проверяет её на всём диапазоне 0 – 9 999 лет — получаем три нужных числа. "Serge3leo
18.05.2025 06:39Звучит, конечно, привлекательно, но может какая-то путаница? По-моему врёт: https://godbolt.org/z/hr83TePq1
KvanTTT
18.05.2025 06:39Стоит ли оно того?
Разве что для фана, поучения теоретического результата.
Нужно ли нам заменять, например, реализацию datetime CPython на этот трюк?
Нет — у полученной формулы отсутствует читаемость, и используется такая операция не так часто, чтобы так заморачиваться. Думаю существует намного больше мест, оптимизация которых принесет намного существенный результат. Тем более ускорение достигается не в 100% случаев.
8street
18.05.2025 06:39Еще быстрее:
создать 100кб файлик и загрузить его в память. В файлике - массив бит, предварительно вычисленных, високосный ли год. Если високосный то 1, если нет то 0. Индекс бита равен номеру года. Обращаемся к массиву по индексу: leap = array[year];
tyomitch
18.05.2025 06:39Здесь все биты в t равны 1, поэтому результат равен true, если какой-то из битов B имеет в p ненулевое значение.
Пардон, но при переводе смысл изменился на противоположный:
Here all bits in t are 1, so the result is true as soon as any bit in B is unset in p.
n0isy
18.05.2025 06:39Почему никто не написал, что тернарный оператор ? от if не отличается? Поэтому и бенчи такие странные.
Serge3leo
Утверждение, конечно, замечательное. И, формально, поскольку григорианский календарь введён только в 1582 году, исторические даты имеют положительный год. Однако, католики, местами, перевели и более ранние даты в григорианский календарь.
Соответственно, может иметь некоторый смысл и определение високосности григорианского года в типе `int32_t` и в интервале порядка `-9999... 9999` или типа того. Интересно, кто-нибудь такое уже смотрел?