Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Benchmark DateTime

Am bit curious about the following behavior when attempting to round off Minutes/Seconds to nearest Hour (in this case ignore the minute/second part).

I tried two approaches, and benchmarked both with BenchMarkDotNet.

private DateTime testData = DateTime.Now;

[Benchmark]
public DateTime CeilingUsingNewOperator() => new DateTime(testData.Year,testData.Month,testData.Day,testData.Hour + 1,0,0);
[Benchmark]
public DateTime CeilingUsingAddOperator() => testData.AddHours(1).AddMinutes(-testData.Minute).AddSeconds(-testData.Second);

Following were the results.

+-------------------------+-----------+----------+----------+
|         Method          |   Mean    |  Error   |  StdDev  |
+-------------------------+-----------+----------+----------+
| CeilingUsingNewOperator | 207.99 ns | 3.465 ns | 3.072 ns |
| CeilingUsingAddOperator | 108.74 ns | 1.429 ns | 1.337 ns |
+-------------------------+-----------+----------+----------+

I was curious why the New Operator is slower, if I were to assume, each time we invoke the AddX method in second approach, we get a Datetime.

Could someone shed some light ?

like image 332
Anu Viswan Avatar asked Apr 11 '18 04:04

Anu Viswan


1 Answers

Further benchmarks could be used to validate this, but I strongly suspect it's because the AddHours, AddMinutes and AddSeconds methods can all work without performing any complex date/time arithmetic. They just need to:

  • Multiply the number of ticks in the unit (hours, minutes, seconds) by the number of units specified in the method cal
  • Add that to the number of ticks in the original DateTime
  • Create a new DateTime value with that new number of ticks

You're also using the Minute and Second properties, but they can be computed without working out month/day/year values.

Compare that with the "single constructor" call which needs to:

  • Work out the year (expensive)
  • Work out the month (expensive)
  • Work out the day (expensive)
  • Work out the hour (cheap)
  • Take a new year/month/day/hour/minute/second and work out the number of ticks (expensive)

The code to work out the year, month and day needs to "understand" the Gregorian calendar - there are far more complex calculations to perform in order to extract that information. Some can be cached (e.g. the number of ticks at the start of each year) but then you lose some locality of reference in terms of memory access.

I'd expect the most efficient approach to be one which computed the ticks directly, just with simple arithmetic:

long originalTicks = testData.Ticks;
long hoursSinceEpoch = originalTicks / TimeSpan.TicksPerHour;
long newTicks = hoursSinceEpoch * TimeSpan.TicksPerHour;
return new DateTime(newTicks, testData.Kind);

One significant downside of that: I believe it will have problems around daylight saving boundaries when the DateTimeKind is Local, as there are really four kinds rather than the three we normally use (Utc, Local, Unspecified). Local is effectively split in two to "know" which date/time is being represented when the value is ambiguous between an earlier option and a later one. If you're not using a Local kind, it should be okay though.

like image 199
Jon Skeet Avatar answered Oct 12 '22 09:10

Jon Skeet