如何在C语言中以编程方式找到闰年

13

我写了一个用C语言编写的程序来判断输入的年份是否为闰年,但不幸的是,它没有正常工作。它会说一年是闰年,而前一年不是。

#include <stdio.h>
#include <conio.h>

int yearr(int year);

void main(void) {
    int year;
    printf("Enter a year:");
    scanf("%d", &year);
    if (!yearr(year)) {
        printf("It is a leap year.");
    } else {
        printf("It is not a leap year");
    }

    getch();
}

int yearr(int year) {
    if ((year % 4 == 0) && (year / 4 != 0))
        return 1;
    else
        return 0;
}

阅读了评论后,我编辑了我的代码如下:

#include <stdio.h>
#include <conio.h>

int yearr(int year);

void main(void) {
    int year;
    printf("Enter a year:");
    scanf("%d", &year);
    if (!yearr(year)) {
        printf("It is a leap year.");
    } else {
        printf("It is not a leap year");
    }

    getch();
}

int yearr(int year) {
    if (year % 4 == 0) {
        if (year % 400 == 0)
            return 1;
        if (year % 100 == 0)
            return 0; 
    } else
        return 0;
}

1
它起作用了吗?此外,代码的可读性很重要,因此yearr是一个查找闰年的函数的不好的名称。在C中,main返回int而不是void - Alok Singhal
编译修订后的代码时,GCC 提示:“在函数‘yearr’中: yearr.c:12: 警告:未到达非 void 函数末尾”。如果您正确缩进代码,则更容易看出原因——如果年份可被4整除但不是100的倍数,则您不会告诉调用者那一年是否为闰年。 - Jonathan Leffler
如果(!yearr(year)) { printf("这是一个闰年。"); } else { printf("这不是一个闰年。"); }相比之下,你不认为下面的代码更易于理解吗? if(yearr(year)) { printf("这不是一个闰年。"); } else { printf("这是一个闰年。"); } - Teju MB
2
K&R 第2.5节: (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 - Susam Pal
16个回答

133

最高效的闰年判断方法:

if ((year & 3) == 0 && ((year % 25) != 0 || (year & 15) == 0))
{
    /* leap year */
}

这段代码在C、C++、C#、Java和许多其他类似C的语言中都是有效的。该代码利用一个由三个独立测试组成的单个TRUE/FALSE表达式:

  • 第四年的测试:year & 3
  • 第100年的测试:year % 25
  • 第400年的测试:year & 15

如下完整讨论了这段代码的运作方式,但首先需要讨论一下维基百科的算法:

维基百科的算法效率低下/不可靠

维基百科发布了一个伪代码算法(请参见:Wikipedia: Leap year - Algorithm),该算法经过持续编辑、意见和破坏。

不要实现维基百科算法!

最长使用的(而且效率低下)维基百科算法如下:

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%的时间里不能被4整除,所以算法在四分之三的情况下只需要进行一次测试而结束。

注:我曾与各种维基百科编辑人员争论改进已发布的算法,认为许多初学者—和职业—程序员很快就会到达维基百科页面(由于搜索引擎的前列),并且实施维基百科伪代码而没有进行任何进一步的研究。维基百科编辑驳回并删除了我改进、注释甚至仅是脚注已发布算法的每一个尝试。显然,他们认为找到效率是程序员的问题。这可能是真的,但许多程序员太匆忙了,无法进行扎实的研究!

关于“最高效”的闰年测试的讨论

使用按位与代替取模:

我用按位与运算符替换了维基百科算法中的两个取模操作。为什么?怎么做?

执行取模计算需要除法。当编写 PC 程序时,这通常不会令人多想几次,但当编写嵌入在小型设备中的 8 位微控制器时,您可能会发现 CPU 无法本地执行除法函数。在这样的 CPU 上,除法是一个困难的过程,涉及重复循环、位移和加/减操作,速度非常慢。因此尽可能避免使用它是非常有帮助的。

事实证明,二的幂次方的余数可以使用按位与运算符交替实现(参见: 维基百科:模运算-性能问题):

x % 2^n == x & (2^n -1)

许多优化编译器会为您将这样的模运算转换为按位与运算符,但较小且不太受欢迎的 CPU 的较少高级编译器可能不会这样做。在每个 CPU 上,按位与是一个单一的指令。

通过将取模4取模400测试替换为&3&15(见下面:“因式分解以减少数学”),我们可以确保最快的代码结果而不使用更慢的除法操作。

不存在任何一个等于100的二次幂。因此,我们被迫继续使用模运算来进行第100年的测试,但是100被25替换(见下文)。

因式分解以简化数学:

除了使用按位与运算符替换取模运算之外,您可能会注意到维基百科算法和优化表达式之间的另外两个争议:

  • 取模100取模25所取代
  • 取模400& 15所取代
第100年测试使用模数25而非模数100。我们可以这样做,因为100可分解为2x2x5x5。由于第4年测试已经检查过4的因子,我们可以从100中消除该因子,留下25。这种优化对几乎所有CPU实现来说可能都不重要(因为100和25都适合8位)。
第400年测试使用& 15代替模数16。同样,我们之所以能够这样做,是因为400可分解为2x2x2x2x5x5。我们可以消除25的因子,因为它被第100年测试检测到,剩下16。我们不能进一步减少16,因为8是200的因子,因此去除任何更多的因子会产生200年的错误结果。
对于8位CPU,第400年的优化非常重要,首先是因为它避免了除法;但更重要的是,值400是一个9位数,在8位CPU中处理起来更加困难。
短路逻辑AND('&&')和OR('||')运算符是最后也是最重要的优化,它们在大多数类C语言中都有实现(参见:维基百科:短路求值)。短路运算符之所以被称为这个名字,是因为如果左侧的表达式单独决定了操作的结果,它们就不会再去评估右侧的表达式。
例如:如果年份是2003年,则year & 3 == 0为false。右侧的测试不可能使结果为true,因此不会再进行任何其他的评估。
通过首先执行第4年测试,只有四分之三(75%)的时间进行了简单的位AND测试。这大大加快了程序的执行速度,尤其是因为它避免了需要进行除法的第100年测试(模25操作)。
关于括号放置的说明:
一位评论者认为我的代码中括号放错了位置,并建议在逻辑AND运算符周围重新组合子表达式(而不是在逻辑OR周围),如下所示:
if (((year & 3) == 0 && (year % 25) != 0) || (year & 15) == 0) { /* LY */ }
上面的内容是不正确的。逻辑 AND 运算符的优先级高于逻辑 OR,并且无论是否加上新括号,它都将首先被评估。在逻辑 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++程序员须知

C/C++程序员可能认为这个表达式更加优化:

if (!(year & 3) && ((year % 25) || !(year & 15))) { /* LY */ }

这并不更加优化!虽然显式的== 0!= 0测试已被删除,但它们变成了隐式的测试并仍然被执行。更糟糕的是,在像C#这样的强类型语言中,year & 3会被计算为一个int,但逻辑AND(&&),OR(||)和NOT(!)运算符需要bool参数,因此代码将不再有效。


4
维基百科不可靠,因为它经常被匿名人员编辑。我曾看到过错误的算法编辑。维基百科不是一个可信的信息来源,而且经常错误或者充满观点倾向。应该把它视为参考,但不是权威。 - Kevin P. Rice
@rindeal 虽然我相信你的代码是可行的,但是括号的放置是错误的。即使第四年的测试失败,也会不必要地评估第400年的测试。你消除了第一个逻辑AND的短路优化。 - Kevin P. Rice
@KevinP.Rice 哦,我现在明白你的意思了,这里应该是修复后的版本。感谢你注意到了这个问题,这段代码也是我的片段库的一部分,我一直在寻找改进它的方法。 - rindeal
@KevinP.Rice:非常好的解释。唯一的评论是:year&15可以替换为year&12,因为此时只有第3位(4)或第4位(8)可能被设置。 - Charles Plager
1
@CharlesPlager 谢谢。我相信,在我编写这个时,我确实意识到使用较小的整数是可能的。我真的很喜欢你的敏锐度!我感激你花时间来审查逻辑并检查我的工作。然而,更改并没有减少操作数中的位数。因此,在我看来,这将是一种毫无意义的混淆,而不是优化。 - Kevin P. Rice
显示剩余17条评论

17

你用来判断闰年的逻辑是错误的。这里有一些信息可以帮助你开始(来自维基百科):

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

x模y的余数表示xy除后的剩余部分。例如,12模5的余数为2。


请查看此处的讨论:https://dev59.com/hnA75IYBdhLWcg3wkaCA#11595914。 - Gert Arnold

9
许多答案谈论了性能,但没有展示任何测量结果。gcc文档中__builtin_expect的一句很好的引用说道:
程序员在预测程序实际执行时通常表现糟糕。
大多数实现使用&&||的短路计算作为优化工具,并继续规定“最佳”性能下除法检查的“正确”顺序。值得一提的是,短路计算不一定是优化特性。

同意,有些检查可能会给出明确的答案(例如年份不是4的倍数),从而使后续测试变得无用。在这一点上立即返回似乎是很合理的,而不是继续进行不必要的计算。另一方面,早期返回引入了分支,这可能会降低性能。(参见这个传奇post。)分支预测和不必要计算之间的权衡非常难以猜测。实际上,它取决于硬件、输入数据、编译器生成的准确汇编指令(可能会因版本而异)等。

接下来将展示在quick-bench.com中获得的测量结果。在所有情况下,我们测量检查存储在std::array<int, 65536>中的每个值是否为闰年所需的时间。这些值是伪随机的,均匀分布在区间[-400,399]内。更确切地说,它们是由以下代码生成的:

auto const years = [](){
  std::uniform_int_distribution<int> uniform_dist(-400, 399);
  std::mt19937 rng;
  std::array<int, 65536> years;
  for (auto& year : years)
    year = uniform_dist(rng);
  return years;
}();

即使可能,我也不会用&代替%(例如,year & 3 == 0而不是year % 4 == 0)。我相信编译器(GCC-9.2在-O3下)会为我完成这个任务。(它确实做到了。)

4-100-400测试

闰年检查通常是以三个可除性测试的形式编写的:year % 4 == 0year % 100 != 0year % 400 == 0。以下是涵盖所有可能出现的检查顺序的实现列表。每个实现都有一个相应的标签前缀。 (某些顺序允许两种不同的实现,在这种情况下,第二个实现会带有后缀b。)它们是:

b4_100_400  : year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
b4_100_400b : (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
b4_400_100  : year % 4 == 0 && (year % 400 == 0 || year % 100 != 0)
b100_4_400  : (year % 100 != 0 && year % 4 == 0) || year % 400 == 0
b100_400_4  : (year % 100 != 0 || year % 400 == 0) && year % 4 == 0
b400_4_100  : year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
b400_100_4  : year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)
b400_100_4b : (year % 400 == 0 || year % 100 != 0) && year % 4 == 0

以下是结果。 (请查看直播。)

4-100-400 tests

与许多人建议的相反,首先检查能否被4整除似乎不是最好的选择。相反,在这些测量中,前三个条形图中的至少五个最差。最好的是
b100_400_4  : (year % 100 != 0 || year % 400 == 0) && year % 4 == 0

4-25-16测试

另一个提供的提示(我必须承认我认为这是一个不错的提示)是将 year % 100 != 0 替换为 year % 25 != 0。这不影响正确性,因为我们还会检查 year % 4 == 0。(如果一个数字是 4 的倍数,那么对于 100 的整除性等同于对于 25 的整除性。)同样,由于存在对 25 的整除性检查,year % 400 == 0 可以被替换为 year % 16 == 0

与上一节类似,我们有8个使用4-25-16可整除性检查的实现:

b4_25_16  : year % 4 == 0 && (year % 25 != 0 || year % 16 == 0)
b4_25_16b : (year % 4 == 0 && year % 25 != 0) || year % 16 == 0
b4_16_25  : year % 4 == 0 && (year % 16 == 0 || year % 25 != 0)
b25_4_16  : (year % 25 != 0 && year % 4 == 0) || year % 16 == 0
b25_16_4  : (year % 25 != 0 || year % 16 == 0) && year % 4 == 0
b16_4_25  : year % 16 == 0 || (year % 4 == 0 && year % 25 != 0)
b16_25_4  : year % 16 == 0 || (year % 25 != 0 && year % 4 == 0)
b16_25_4b : (year % 16 == 0 || year % 25 != 0) && year % 4 == 0

结果(实时这里):

4-25-16 tests

再次,首先检查能否被4整除似乎不是一个好主意。在这轮比赛中,更快的方法是

b25_16_4  : (year % 25 != 0 || year % 16 == 0) && year % 4 == 0

4-100-400测试(无分支)

如前所述,分支可能会降低性能。特别是,短路可能会适得其反。当出现这种情况时,一个经典的技巧是用位运算符 & 和 | 替换逻辑运算符 && 和 ||。实现如下:

nb4_100_400  : (year % 4 == 0) & ((year % 100 != 0) | (year % 400 == 0))
nb4_100_400b : ((year % 4 == 0) & (year % 100 != 0)) | (year % 400 == 0)
nb4_400_100  : (year % 4 == 0) & ((year % 400 == 0) | (year % 100 != 0))
nb100_4_400  : ((year % 100 != 0) & (year % 4 == 0)) | (year % 400 == 0)
nb100_400_4  : ((year % 100 != 0) | (year % 400 == 0)) & (year % 4 == 0)
nb400_4_100  : (year % 400 == 0) | ((year % 4 == 0) & (year % 100 != 0))
nb400_100_4  : (year % 400 == 0) | ((year % 100 != 0) & (year % 4 == 0))
nb400_100_4b : ((year % 400 == 0) | (year % 100 != 0)) & (year % 4 == 0)

结果 (实时这里):

4-100-400 tests (no branching)

一个显著的特点是性能变化不像分支情况那样明显,这使得宣布获胜者变得困难。我们选择了这个。
nb100_400_4  : ((year % 100 != 0) | (year % 400 == 0)) & (year % 4 == 0)

4-25-16测试(无分支)

为了完成这个练习,我们考虑使用4-25-16可除性测试的无分支情况:

nb4_25_16  : (year % 4 == 0) & ((year % 25 != 0) | (year % 16 == 0))
nb4_25_16b : ((year % 4 == 0) & (year % 25 != 0)) | (year % 16 == 0)
nb4_16_25  : (year % 4 == 0) & ((year % 16 == 0) | (year % 25 != 0))
nb25_4_16  : ((year % 25 != 0) & (year % 4 == 0)) | (year % 16 == 0)
nb25_16_4  : ((year % 25 != 0) | (year % 16 == 0)) & (year % 4 == 0)
nb16_4_25  : (year % 16 == 0) | ((year % 4 == 0) & (year % 25 != 0))
nb16_25_4  : (year % 16 == 0) | ((year % 25 != 0) & (year % 4 == 0))
nb16_25_4b : ((year % 16 == 0) | (year % 25 != 0)) & (year % 4 == 0)

结果(实时这里):

4-25-16 tests (no branching)

再次强调,"最好" 是难以定义的,我们选择了这个:

nb25_16_4  : ((year % 25 != 0) | (year % 16 == 0)) & (year % 4 == 0)

欧洲冠军联赛

现在是挑选之前各个部分最佳并进行比较的时候了:

b100_400_4  : (year % 100 != 0 || year % 400 == 0) && year % 4 == 0
b25_16_4    : (year % 25 != 0 || year % 16 == 0) && year % 4 == 0
nb100_400_4 : ((year % 100 != 0) | (year % 400 == 0)) & (year % 4 == 0)
nb25_16_4   : ((year % 25 != 0) | (year % 16 == 0)) & (year % 4 == 0)

结果(实时这里):

Champions League

这张图表建议,短路是一种优化,但除以4应该是最后检查的而不是首先检查的。为了更好的性能,应该先检查能否被100整除。这相当令人惊讶!毕竟,后一个测试永远不能足以判断是否闰年并且随后需要进行另一次测试(通过400或通过4)。

同样让人惊讶的是,对于使用简单除数 2516 的分支版本来说,比起使用更直观的 100400 并没有更好。我可以提供我的“理论”,它也部分解释了为什么先测试 100 要比测试 4 更好。正如许多人指出的那样,通过 4 进行可除性测试将执行分成(25%,75%)两部分,而通过 100 进行测试将其分成(1%,99%)。虽然在后一次检查之后,执行必须继续进行另一个测试,但至少分支预测器更有可能猜测出正确的方向。同样,通过 25 进行可除性检查将执行分成(4%,96%),这对于分支预测器来说比(1%,99%)更具挑战性。看起来最好最小化分布的熵,帮助分支预测器,而不是最大化尽早返回的概率。

对于无分支版本,简化的除数确实提供更好的性能。在这种情况下,分支预测器没有任何作用,因此越简单越好。一般来说,编译器可以使用更小的数字进行更好的优化。

这就是它吗?

我们撞到了墙,发现

b100_400_4  : (year % 100 != 0 || year % 400 == 0) && year % 4 == 0

最高效的闰年检查方法吗?绝对不是。例如,我们没有混合使用分支运算符&&||和非分支运算符&|。也许... 让我们来看看并将其与其他两个实现进行比较。第一个是

m100_400_4 : (year % 100 != 0 || year % 400 == 0) & (year % 4 == 0)

注意分支运算符||和非分支运算符&的混合使用。第二种方法是一个晦涩的“技巧”:

bool b;
auto n = 0xc28f5c29 * year;
auto m = n + 0x051eb850;
m = (m << 30 | m >> 2);
if (m <= 0x28f5c28)
  b = n % 16 == 0;
else
  b = n % 4 == 0;
return b

后者能够工作吗?是的,它可以。我建议不要给出数学证明,而是将上面的代码与更易读的源代码生成的代码进行比较。

bool b;
if (year % 100 == 0)
  b = year % 16 == 0;
else
  b = year % 4 == 0;
return b;

编译器资源管理器中。它们几乎完全相同,唯一的区别是一个使用add指令,而另一个使用lea。这应该能让你相信黑客代码确实有效(只要另一个有效)。

基准测试结果(在此处live):

Final

“等等,你为什么不使用上面图片中更易读的代码呢?”你这样问道。好吧,我试过并学到了另一个教训。当这段代码插入基准循环时,编译器会将源代码作为整体来查看,并决定发出与在隔离状态下看到源代码时不同的代码。性能更差了。想象一下吧!
“那现在呢?就这样了吗?”
“我不知道!我们可以探索很多事情。例如,最后一节展示了另一个版本,它使用了if语句而不是短路计算。这可能是获得更好性能的一种方法。我们还可以尝试三元运算符?。”
“请注意,所有测量和结论都基于GCC-9.2。使用另一个编译器和/或版本可能会有所改变。例如,从9.1版本开始,GCC引入了一种新的改进后的可除性检查算法。因此,旧版本具有不同的性能,并且不必要的计算和分支错误预测之间的权衡可能已经改变。”
“我们可以明确得出的结论是:”
  1. 不要过度思考。除非你是为用户提供高效实现的库编写者,否则应优先选择易于理解的代码而不是难以理解的优化。毕竟,手工制作的代码片段可能不是升级编译器时最有效的选项。正如ex-nihilo在评论中所说,“除非已知是性能瓶颈,否则没有必要为这样一段代码苦恼。”
  2. 猜测很困难,最好进行测量。
  3. 测量很困难,但比猜测更好。
  4. 短路/分支对性能有利,但这取决于许多因素,包括代码对分支预测器的帮助程度。
  5. 单独生成的代码片段在内联到其他地方时可能会有所不同。

1
@exnihilo 非常正确。帖子已更新。实际上,这是按重要性排序的第一点。 - Cassio Neri
@exnihilo:您可能会对查看更多解决方案并进行基准测试感兴趣。感谢Cassio指向quick-bench.com。你解救了我的困境 :) - chqrlie
1
@KevinP.Rice:感谢您的评论。我希望我能澄清您的一些疑虑。虽然我从来没有看到过明确说明,但首先检查4的论点似乎认为早返回比不返回要好。我认为这是一种自然假设。然而,它忽略了在许多流行和现代硬件上可能产生巨大影响的分支成本。零成本分支不是我通常必须从支持首先检查4的论据中猜测的唯一隐藏假设。(1/3) - Cassio Neri
1
@HowardHinnant 它使用一种称为模反元素的技术,自clang和gcc 9.x版本以来已经实现。理想情况下,我不需要使用它,只需让编译器处理即可。但正如我在帖子中所说,当上面的片段不是孤立存在而是在基准测试代码(即在测量循环内部)的上下文中时,生成的代码会有所不同。自那时起,事情可能已经发生了变化。(1/2) - Cassio Neri
1
我在这里写了有关模反元素算法的文章(https://accu.org/journals/overload/27/154/overload154.pdf#page=13)。该文章涵盖了无符号整数情况下的主要思路(有符号整数需要额外的步骤)。如果我们可以承受处理不在完整32位范围内的年份(例如std::chrono::year),那么还有另一种更有效的技术,我称之为mcomp(用于乘法和比较)。我在这里写了有关它的文章(https://accu.org/journals/overload/28/155/overload155.pdf#page=16)。同样,这篇文章仅涵盖了无符号整数,而有符号整数需要进行适当调整。 (2/2) - Cassio Neri
显示剩余10条评论

6
int isLeapYear(int year)
{
   return (year % 400 == 0) || ( ( year % 100 != 0) && (year % 4 == 0 ));
}

1
低效率主要是由于执行顺序。必须每次计算Mod 400和Mod 100,而Mod 4项可以在75%的时间内排除它们(请参见我上面的答案)。微不足道?在事情的大局中是的。然而,在寻找参考答案时,维基百科算法经常被重复使用作为一个好的算法(但它并不是很好)。对一些人来说,这个细节并不重要,但对我来说,维基百科算法就像写2/4而不是1/2。 - Kevin P. Rice
3
@KevinP.Rice,这个问题已经两年了,但我还是想回答一下。如果您看到原帖的代码,就会发现他很明显是一个初学者,并没有要求高效的实现。鉴于此,如果我是老师,我不会建议原帖使用您的答案。此外,我总是会选择易读的代码而不是过度优化的代码。但每个人都有自己的喜好。祝你好运! - st0le
4
加油!我不会认为调换项的顺序(将“year%4”放在首位)是微小优化,也不会认为模除尤其适合初学者或易于阅读。我不是“所有代码必须明确可理解”的信徒---那就是代码注释的作用,我会大量注释我的代码!如果最有效的算法已经广泛发表,像E=MC2一样成为经典算法,那么没有人会质疑它的可读性。相反,维基百科上的弱算法已经成为规范,世界陷入停滞。愤怒结束。圣帕特里克节快乐! - Kevin P. Rice

4

这可能是正确的解决方案。 维基百科上给出的算法不正确。

-(BOOL)isLeapYear: (int)year{    

    if(year%4==0){
      if(year%100!=0){
        return YES;
     }
     else if(year%400!=0){
        return YES;
     }
     else return NO;
   }

    else return NO;
  }

4

虽然先除以400的逻辑无可挑剔,但与先除以4相比,它的计算效率不如前者。可以使用以下逻辑来实现:

#define LEAPYEAR(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))

这个算法对每个值都除以4,但对于其中3/4的值,测试就在此终止了。对于通过第一个测试的1/4,它会再次除以100,消除24/25的值;对于剩下的1%,它还会除以400,得出最终答案。尽管如此,这并不能带来很大的节省。


你应该将 y % 4 替换为 y & 3,并将 y % 400 替换为 y & 15。原因是:在某些平台上,模数除法很慢,而单个按位与通常只需要一个指令周期。一些语言编译器会自动执行此优化,但将其嵌入代码可以保证它。模400可以用 & 15 替换,因为16是400的因数,但不是100(或200)的因数。对于8位系统,& 15 更容易处理。从技术上讲,% 100 可以替换为 % 25,但这对任何系统都不太可能有所改进。 - Kevin P. Rice
@Kevin P. Rice 如果整数不是2的补码且y < 0,用y&3替换y%4是否能正常工作? - chux - Reinstate Monica
1
@KevinP.Rice:从儒略日历转换为公历要比"它发生在1582年"复杂得多。许多罗马天主教国家在1584年改变;受英国影响的国家在1752年改变;其他国家在不同的时间改变(俄罗斯在20世纪改变;据我所知,俄罗斯东正教仍未改变)。在类Unix的机器上检查cal 9 1752的输出。此外,对于负年份,您必须处理是否存在年0的问题。 - Jonathan Leffler
1
@KevinP.Rice:看起来我记错了1584年的特殊性;事实上,教皇格里高利在1582年颁布使用格里高利历,大多数罗马天主教国家在那一年开始使用了该历法,但不是全部。 - Jonathan Leffler
1
@Kevin P. Rice y < 1582 在使用普通格里高利历时是有意义的。这就像使用公元1年或100年作为儒略/格里高利历,因为使用基于公元年份的儒略/格里高利历直到几个世纪后可能是在300-400年之后才发展起来的。 - chux - Reinstate Monica
显示剩余2条评论


2
您的代码问题在于,如果您认为该年份是闰年,则从yearr返回非零值。因此,在if语句中不需要使用!

1
I used this code:

#include <stdio.h>

int main()
{
    int yr;
    printf ("Enter a year \n");
    scanf ("%d", &yr);

    if (yr%400 == 0)
        printf("\n LEAP YEAR.");

    else if (yr%4==0 && yr%100!=0)
        printf("\n LEAP YEAR.");
    else
        printf ("\n NOT LEAP YEAR.");
}

1

这里有另外两种解决方案,它们似乎在quick-bench.com基准测试上表现更胜一筹。

其中一种解决方案经过测试,但使用clang编译后生成无分支代码:

int isleap3(int year) {
    unsigned y = year + 16000;
    return (y % 100) ? !(y % 4) : !(y % 16);
}

这个方法只使用了一次取模运算,没有测试,并且编译后只有两次乘法操作:

static unsigned char const leaptest[400] = {
    1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,
    0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,
    0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,
    0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,
    0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,
    0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,
    0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,
    0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,
    0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,
    0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,
    0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,
    0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,
    0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,
};
int isleap4(int year) {
    unsigned y = year + 16000;
    return leaptest[y % 400];
}

clang 64位汇编

isleap3:                                # @isleap3
        add     edi, 16000
        imul    eax, edi, -1030792151
        ror     eax, 2
        cmp     eax, 42949673
        mov     eax, 15
        mov     ecx, 3
        cmovb   ecx, eax
        xor     eax, eax
        test    ecx, edi
        sete    al
        ret
isleap4:                                # @isleap4
        add     edi, 16000
        imul    rax, rdi, 1374389535
        shr     rax, 39
        imul    eax, eax, 400
        sub     edi, eax
        movzx   eax, byte ptr [rdi + leaptest]
        ret
leaptest:
        .asciz  "\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\000\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000\000\001\000\000"

这里是基准测试结果

enter image description here


这很有趣!使用 ? 是获取 cmove 的好方法。(不幸的是,与clang相比,gcc分支更多。)使用 unsigned 是个好主意,因为对于许多操作(特别是 %),它比 int 更快。 - Cassio Neri
然而,您的实现在整个整数范围内都无法正常工作。例如,isleap3对于y = -16100返回true,但-16100不是闰年。(请参见此处。)如果year足够大,则year + 16000会溢出,这对于int来说是未定义的行为。(要解决此问题,您可以在执行计算之前将其强制转换为“unsigned”,即year + 16000u。)请原谅我的珍贵,毕竟,在16000年之前和年份足够大以发生溢出之前,谁需要检查闰年呢? - Cassio Neri
现在,不考虑上述问题,看看下面的难题。(我真的不知道答案。)当我看到isleap3时,我认为通过用y%25替换y%100可以改进它,因为这将删除ror指令。我们可以在这里看到我是正确的。然而,正如我们在这里所看到的那样,性能会下降。怎么回事?isleap3执行与isleap3_modified相同的指令另外还有ror。前者怎么可能比后者更快呢? - Cassio Neri
(在我的第二条评论中,我是指“在-16000之前”。) - Cassio Neri

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接