日期时间.DayOfWeek微优化

22

首先:

  1. 我只是出于好玩和渴望学习的目的提出这个问题。我必须承认,我喜欢折腾微小的优化(尽管它们从未在我的任何开发中导致过任何显著的速度增加)。

  2. DateTime.DayOfWeek 方法在我所有的应用程序中都不是瓶颈。

  3. 在其他任何应用程序中,这也极不可能成为一个问题。如果有人认为这个方法对他的应用程序性能有影响,他应该考虑何时进行优化,然后进行一次分析。

使用 ILSpy 反编译 DateTime 类,我们可以找出 DateTime.DayOfWeek 是如何实现的:

public DayOfWeek DayOfWeek
{
    [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
    get
    {
        return (DayOfWeek)((this.InternalTicks / 864000000000L + 1L) % 7L);
    }
}

public long Ticks
{
    [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    get
    {
        return this.InternalTicks;
    }
}

这种方法执行以下操作:

  1. 将与当前日期对应的刻度数除以一天中现有的刻度数。

  2. 我们将以上结果加1,以使7的余数介于0和6之间。

这是计算星期几的唯一方法吗?

是否可能重新实现以使其运行更快?


请看这个问题:https://dev59.com/MHTYa4cB1Zd3GeqPxKRX 。不同的是,我已经回答了自己的问题。 - rpax
1个回答

83

让我们进行一些调整。

  1. TimeSpan.TicksPerDay (864000000000) 的质因数分解为:Prime factorization of 864000000000

现在可以将DayOfWeek表示为:

public DayOfWeek DayOfWeek
{                   
    get
    {
        return (DayOfWeek)(((Ticks>>14) / 52734375 + 1L) % 7L);
    }
}

我们正在模7运算,52734375 % 7 是1。因此,上面的代码等价于:

public static DayOfWeek dayOfWeekTurbo(this DateTime date)
{
    return (DayOfWeek)(((date.Ticks >> 14) + 1) % 7);
}

直觉上,它是有效的。但是让我们用代码来证明它。
public static void proof()
{
    DateTime date = DateTime.MinValue;
    DateTime max_date = DateTime.MaxValue.AddDays(-1);
    while (date < max_date)
    {
        if (date.DayOfWeek != date.dayOfWeekTurbo())
        {
            Console.WriteLine("{0}\t{1}", date.DayOfWeek, date.dayOfWeekTurbo());
            Console.ReadLine();
        }
        date = date.AddDays(1);
    }
}

如果您想运行它,可以运行,但我向您保证它能正常工作。

好的,唯一剩下的就是一些基准测试。

这是一个辅助方法,为了让代码更清晰:

public static IEnumerable<DateTime> getAllDates()
{
    DateTime d = DateTime.MinValue;
    DateTime max = DateTime.MaxValue.AddDays(-1);
    while (d < max)
    {
        yield return d;
        d = d.AddDays(1);
    }
}

我想这不需要解释。
public static void benchDayOfWeek()
{

    DateTime[] dates = getAllDates().ToArray();
    // for preventing the compiler doing things that we don't want to
    DayOfWeek[] foo = new DayOfWeek[dates.Length];
    for (int max_loop = 0; max_loop < 10000; max_loop+=100)
    {


        Stopwatch st1, st2;
        st1 = Stopwatch.StartNew();
        for (int i = 0; i < max_loop; i++)
            for (int j = 0; j < dates.Length; j++)
                foo[j] = dates[j].DayOfWeek;
        st1.Stop();

        st2 = Stopwatch.StartNew();
        for (int i = 0; i < max_loop; i++)
            for (int j = 0; j < dates.Length; j++)
                foo[j] = dates[j].dayOfWeekTurbo();
        st2.Stop();

        Console.WriteLine("{0},{1}", st1.ElapsedTicks, st2.ElapsedTicks);

    }
    Console.ReadLine();
    Console.WriteLine(foo[0]);

}

输出:

96,28
172923452,50884515
352004290,111919170
521851120,168153321
683972846,215554958
846791857,264187194
1042803747,328459950
Monday

如果我们用数据制作图表,它看起来像这样:

Chart

╔══════════════════════╦════════════════════╦═════════════════════╦═════════════╗
║ Number of iterations ║ Standard DayOfWeek ║ Optimized DayOfWeek ║   Speedup   ║
╠══════════════════════╬════════════════════╬═════════════════════╬═════════════╣
║                    0 ║                 96 ║                  28 ║ 3.428571429 ║
║                  100 ║          172923452 ║            50884515 ║ 3.398351188 ║
║                  200 ║          352004290 ║           111919170 ║ 3.145165301 ║
║                  300 ║          521851120 ║           168153321 ║ 3.103424404 ║
║                  400 ║          683972846 ║           215554958 ║ 3.1730787   ║
║                  500 ║          846791857 ║           264187194 ║ 3.205272156 ║
║                  600 ║         1042803747 ║           328459950 ║ 3.174827698 ║
╚══════════════════════╩════════════════════╩═════════════════════╩═════════════╝

速度提升3倍。

注意:代码使用Visual Studio 2013编译,以发布模式运行,并在除应用程序外关闭所有内容(当然包括VS)。

我在东芝Satellite C660-2JK上进行了测试, 采用英特尔® Core™ i3-2350M处理器和Windows® 7 Home Premium 64位操作系统。

编辑:

正如Jon Skeet所指出的那样,该方法在不处于日期边界时可能会失败。

由于Jon Skeet的评论,这个答案被修改了:

dayOfWeekTurbo在不处于日期边界时可能会失败。例如, 考虑new DateTime(2014, 3, 11, 21, 39, 30)——你的方法认为 它是星期五,但实际上是星期二。"我们在模7下工作"是错误的方式...通过去掉额外的 除法,星期几会在一天之中改变。

因此,我决定进行编辑。

如果我们更改proof()方法,

public static void proof()
{
    DateTime date = DateTime.MinValue;
    DateTime max_date = DateTime.MaxValue.AddSeconds(-1);
    while (date < max_date)
    {
        if (date.DayOfWeek != date.dayOfWeekTurbo2())
        {
            Console.WriteLine("{0}\t{1}", date.DayOfWeek, date.dayOfWeekTurbo2());
            Console.ReadLine();
        }
        date = date.AddSeconds(1);
    }
}

失败啦!

Jon Skeet 是正确的。让我们遵循 Jon Skeet 的建议,并应用这个除法。

public static DayOfWeek dayOfWeekTurbo2(this DateTime date)
{
    return (DayOfWeek)((((date.Ticks >> 14) / 52734375L )+ 1) % 7);
}

此外,我们改变了getAllDates()方法。

public static IEnumerable<DateTime> getAllDates()
{
    DateTime d = DateTime.MinValue;
    DateTime max = DateTime.MaxValue.AddHours(-1);
    while (d < max)
    {
        yield return d;
        d = d.AddHours(1);
    }
}

并且 benchDayOfWeek()

public static void benchDayOfWeek()
{

    DateTime[] dates = getAllDates().ToArray();
    DayOfWeek[] foo = new DayOfWeek[dates.Length];
    for (int max_loop = 0; max_loop < 10000; max_loop ++)
    {


        Stopwatch st1, st2;
        st1 = Stopwatch.StartNew();
        for (int i = 0; i < max_loop; i++)
            for (int j = 0; j < dates.Length; j++)
                foo[j] = dates[j].DayOfWeek;
        st1.Stop();

        st2 = Stopwatch.StartNew();
        for (int i = 0; i < max_loop; i++)
            for (int j = 0; j < dates.Length; j++)
                foo[j] = dates[j].dayOfWeekTurbo2();
        st2.Stop();

        Console.WriteLine("{0},{1}", st1.ElapsedTicks, st2.ElapsedTicks);

    }
    Console.ReadLine();
    Console.WriteLine(foo[0]);

}

速度仍然会更快吗?答案是是的

输出:

90,26
43772675,17902739
84299562,37339935
119418847,47236771
166955278,72444714
207441663,89852249
223981096,106062643
275440586,125110111
327353547,145689642
363908633,163442675
407152133,181642026
445141584,197571786
495590201,217373350
520907684,236609850
511052601,217571474
610024381,260208969
637676317,275558318

Chart

╔══════════════════════╦════════════════════╦════════════════════════╦═════════════╗
║ Number of iterations ║ Standard DayOfWeek ║ Optimized DayOfWeek(2) ║  Speedup    ║
╠══════════════════════╬════════════════════╬════════════════════════╬═════════════╣
║                    143772675179027392.445026708 ║
║                    284299562373399352.257624766 ║
║                    3119418847472367712.528090817 ║
║                    4166955278724447142.304588821 ║
║                    5207441663898522492.308697504 ║
║                    62239810961060626432.111781205 ║
║                    72754405861251101112.201585338 ║
║                    83273535471456896422.246923958 ║
║                    93639086331634426752.226521519 ║
║                   104071521331816420262.241508433 ║
║                   114451415841975717862.25306251  ║
║                   124955902012173733502.279903222 ║
║                   135209076842366098502.201546909 ║
║                   145110526012175714742.348895246 ║
║                   156100243812602089692.344363391 ║
║                   166376763172755583182.314124725 ║
╚══════════════════════╩════════════════════╩════════════════════════╩═════════════╝

速度提升两倍。


4
我在这里找到了它:http://www.sensefulsolutions.com/2010/10/format-text-as-table.html。 它已经在我的书签栏上了 :) - rpax
11
dayOfWeekTurbo不在日期边界时可能会失败。例如,考虑new DateTime(2014, 3, 11, 21, 39, 30) - 您的方法认为它是星期五,但实际上是星期二。"我们使用模7进行计算"的方向是错误的,基本上...通过删除额外的除法,一天中的星期几会在一天内变化 - Jon Skeet
1
Skeet 是绝对正确的。原始的 long 除法 InternalTicks / TimeSpan.TicksPerDay 具有截断值内部的时间部分的重要特征。您的子表达式 date.Ticks >> 142**14 == 16384 tick "增加" 一次,因此每经过 1.6384 毫秒,您将获得一个新的星期几? - Jeppe Stig Nielsen

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