Ответ 2
Самый эффективный тест на летальный год:
if ((year & 3) == 0 && ((year % 25) != 0 || (year & 15) == 0))
{
/* leap year */
}
Этот код действителен в C, С++, С#, Java и многих других языках, подобных C. В коде используется одно выражение TRUE/FALSE, состоящее из трех отдельных тестов:
- 4-й год:
year & 3
- 100-летний тест:
year % 25
- 400-летний тест:
year & 15
Полное обсуждение того, как работает этот код, приводится ниже, но сначала обсуждение алгоритма Википедии вызывается для:
Алгоритм Википедии НЕВОЗМОЖНО/НЕПОСРЕДСТВЕННО
Википедия опубликовала псевдокодовый алгоритм (см.: Википедия: Високосный год - Алгоритм), который подвергался постоянному редактированию, и вандализм.
НЕ ВЫПОЛНЯЙТЕ АЛГОРИТМ WIKIPEDIA!
Один из самых длинных (и неэффективных) алгоритмов Википедии появился следующим образом:
if year modulo 400 is 0 then
is_leap_year
else if year modulo 100 is 0 then
not_leap_year
else if year modulo 4 is 0 then
is_leap_year
else
not_leap_year
Вышеуказанный алгоритм неэффективен, потому что он всегда выполняет тесты на 400-й год и 100-й год даже в течение многих лет, которые быстро не пройдут "тест 4-го года" (тест по модулю 4), что составляет 75% времени! Переупорядочив алгоритм для выполнения 4-годичного теста, мы значительно ускорим процесс.
"САМЫЙ ЭФФЕКТИВНЫЙ" АЛГОРИТМ ПСЕВДО-КОДА
Я предоставил Википедию следующий алгоритм (более одного раза):
if year is not divisible by 4 then not leap year
else if year is not divisible by 100 then leap year
else if year is divisible by 400 then leap year
else not leap year
Этот "самый эффективный" псевдокод просто изменяет порядок тестов, поэтому сначала происходит деление на 4, за которым следуют менее часто встречающиеся тесты. Поскольку "год" не делит на четыре 75 процентов времени, алгоритм заканчивается только после одного теста в трех из четырех случаев.
ПРИМЕЧАНИЕ: Я боролся с различными редакторами Википедии, чтобы улучшить опубликованный там алгоритм, утверждая, что многие новички и профессионалы и программисты быстро приходят на страницу Википедии (из-за лучших списков поисковых систем) и внедряют псевдокод Wikipedia без каких-либо дальнейших исследований. Редакторы Википедии отреклись от всех попыток, которые я сделал, чтобы улучшить, аннотировать или даже просто записать опубликованный алгоритм. По-видимому, они считают, что найти эффективность - проблема программиста. Это может быть правдой, но многие программисты слишком торопились, чтобы выполнить прочные исследования!
ОБСУЖДЕНИЕ ИСПЫТАНИЯ ГОДА "МОСТ-ЭФФЕКТИВНЫЙ"
Побитовое - и вместо modulo:
Я заменил две из операций по модулю в алгоритме Википедии с побитовыми-И-операциями. Почему и как?
Для вычисления по модулю требуется деление. Во время программирования ПК часто не так много думать об этом, но при программировании 8-разрядных микроконтроллеров, встроенных в небольшие устройства, вы можете обнаружить, что функция разделения не может быть изначально выполнена процессором. На таких процессорах разделение представляет собой сложный процесс, включающий повторение цикла, смещение битов и добавление/вычитание операций, которые очень медленные. Очень желательно избегать.
Оказывается, что по модулю степеней двух можно поочередно достичь с помощью операции поразрядного И (см. Википедия: Работа в модуле - Проблемы с производительностью):
x% 2 ^ n == x и (2 ^ n - 1)
Многие оптимизирующие компиляторы преобразуют такие операции modulo в bitwise-AND для вас, но менее продвинутые компиляторы для меньших и менее популярных процессоров не могут. Побитовое - И представляет собой одну инструкцию для каждого процессора.
Заменив теги modulo 4
и modulo 400
на & 3
и & 15
(см. ниже: "Факторинг для сокращения математики" ), мы можем гарантировать, что самый быстрый код будет работать без использования операции более медленного деления.
Существует не две силы, равные 100. Таким образом, мы вынуждены продолжать использовать модульную операцию для 100-летнего теста, однако 100 заменяется на 25 (см. ниже).
Факторинг для упрощения математики:
В дополнение к использованию побитового-И для замены операций modulo вы можете отметить два дополнительных спора между алгоритмом Википедии и оптимизированным выражением:
-
modulo 100
заменяется на modulo 25
-
modulo 400
заменяется на & 15
100-летний тест использует modulo 25
вместо modulo 100
. Мы можем сделать это, потому что 100 факторов равны 2 x 2 x 5 x 5. Поскольку 4-летний тест уже проверяет факторы 4, мы можем исключить этот коэффициент из 100, оставив 25. Эта оптимизация, вероятно, незначительна почти для каждой реализации ЦП ( поскольку как 100, так и 25 подходят в 8 бит).
В 400-летнем тесте используется & 15
, что эквивалентно modulo 16
. Опять же, мы можем это сделать, потому что 400 факторов составляют 2 x 2 x 2 x 2 x 5 x 5. Мы можем устранить коэффициент 25, который проверяется на 100-летнем тесте, оставляя 16. Мы не можем далее уменьшить 16, потому что 8 в 200 раз, поэтому удаление каких-либо факторов приведет к нежелательному положительному результату на 200-й год.
Оптимизация на 400 лет очень важна для 8-разрядных ЦП, во-первых, потому что она позволяет избежать деления; но, что более важно, поскольку значение 400 представляет собой 9-разрядное число, с которым гораздо сложнее справиться в 8-разрядном процессоре.
Локальные логические операторы AND/OR:
Конечной и наиболее важной используемой оптимизацией являются операторы короткого замыкания AND ('& &') и OR ('||') (см. Википедия: оценка короткого замыкания), которые реализованы на большинстве C-подобных языков. Операторы короткого замыкания названы так потому, что они не хотят оценивать выражение с правой стороны, если выражение на левой стороне само по себе определяет результат операции.
Например: если год 2003, то year & 3 == 0
- false. Нет никакого способа, чтобы тесты с правой стороны логического И могли сделать результат истинным, поэтому ничего другого не оценили.
Выполняя сначала тест на 4-й год, только четвертый тест (простой побитовый-И) оценивается три четверти (75 процентов) времени. Это значительно ускоряет выполнение программы, тем более что она позволяет избежать деления, необходимого для теста 100-го года (операция по модулю 25).
ПРИМЕЧАНИЕ НА РАЗМЕЩЕНИЕ РОДИТЕЛЕЙ
Один комментатор считает, что круглые скобки были неуместны в моем коде и предложили, чтобы подвыражения были перегруппированы вокруг логического оператора И (а не вокруг логического ИЛИ), как показано ниже:
if (((year & 3) == 0 && (year % 25) != 0) || (year & 15) == 0) { /* LY */ }
Вышеуказанное неверно. Логический оператор И имеет более высокий приоритет, чем логический ИЛИ, и будет оцениваться сначала с новыми скобками или без них. Скобки вокруг логических аргументов AND не влияют. Это может привести к тому, что полностью исключить подгруппы:
if ((year & 3) == 0 && (year % 25) != 0 || (year & 15) == 0) { /* LY */ }
Но в случае обоих правильная сторона логического ИЛИ (тест 400-го года) оценивается почти каждый раз (т.е. годы не делятся на 4 и 100). Таким образом, полезная оптимизация была ошибочно устранена.
Скобки в моем исходном коде реализуют наиболее оптимизированное решение:
if ((year & 3) == 0 && ((year % 25) != 0 || (year & 15) == 0)) { /* LY */ }
Здесь логический ИЛИ оценивается только в течение лет, делящихся на 4 (из-за короткого замыкания И). Правая часть логического ИЛИ оценивается только в течение лет, делящихся на 4 и 100 (из-за короткого замыкания ИЛИ).
ПРИМЕЧАНИЕ ДЛЯ ПРОГРАММИРОВАНИЙ C/С++
Программисты C/С++ могут почувствовать, что это выражение более оптимизировано:
if (!(year & 3) && ((year % 25) || !(year & 15))) { /* LY */ }
Это не оптимизировано! Хотя явные тесты == 0
и != 0
удаляются, они становятся неявными и все еще выполняются. Хуже того, код больше не действует в строго типизированных языках, таких как С#, где year & 3
оценивается как int
, но операторы логического AND (&&
), OR (||
) и NOT (!
)) требуют аргументов bool
.