Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Microoptimizations on DateTimeParser #2990

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ BenchmarkDotNet.Artifacts/
_ReSharper.*
*.ReSharper.user
*.resharper.user
*/.idea/*
.vs/
.vscode/
*.lock.json
Expand Down
49 changes: 41 additions & 8 deletions Src/Newtonsoft.Json.Tests/Benchmarks/DeserializeBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,43 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using BenchmarkDotNet.Attributes;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Tests.TestObjects;
using Newtonsoft.Json.Utilities;

namespace Newtonsoft.Json.Tests.Benchmarks
{
public class DeserializeBenchmarks
{
private static readonly string LargeJsonText;
private static readonly string FloatArrayJson;
private static readonly string DateArrayJson;
private static readonly StringReference[] DateTimeStringReferences;
private static readonly DateTimeOffset[] ParsedDateTimesArray;
private static readonly JsonSerializer Serializer = new();

static DeserializeBenchmarks()
{
LargeJsonText = System.IO.File.ReadAllText(TestFixtureBase.ResolvePath("large.json"));

FloatArrayJson = new JArray(Enumerable.Range(0, 5000).Select(i => i * 1.1m)).ToString(Formatting.None);
const int count = 5000;
FloatArrayJson = new JArray(Enumerable.Range(0, count).Select(i => i * 1.1m)).ToString(Formatting.None);
var dates = new DateTime[count];
DateTimeStringReferences = new StringReference[count];
ParsedDateTimesArray = new DateTimeOffset[count];
DateTime time = new(1969, 7, 20, 2, 56, 15, DateTimeKind.Utc);
for (int i = 0; i < count; i++)
{
DateTime dateTime = time.AddDays(i);
dates[i] = dateTime;
string dateTimeString = dateTime.ToString("O", CultureInfo.InvariantCulture);
DateTimeStringReferences[i] = new StringReference(dateTimeString.ToCharArray(), 0, dateTimeString.Length);
}
DateArrayJson = new JArray(dates).ToString(Formatting.None);
}

[Benchmark]
Expand All @@ -59,11 +75,10 @@ public IList<RootObject> DeserializeLargeJsonText()
[Benchmark]
public IList<RootObject> DeserializeLargeJsonFile()
{
using (var jsonFile = System.IO.File.OpenText(TestFixtureBase.ResolvePath("large.json")))
using StringReader jsonFile = new(LargeJsonText);
using (JsonTextReader jsonTextReader = new JsonTextReader(jsonFile))
{
JsonSerializer serializer = new JsonSerializer();
return serializer.Deserialize<IList<RootObject>>(jsonTextReader);
return Serializer.Deserialize<IList<RootObject>>(jsonTextReader);
}
}

Expand All @@ -78,6 +93,24 @@ public IList<decimal> DeserializeDecimalList()
{
return JsonConvert.DeserializeObject<IList<decimal>>(FloatArrayJson);
}

[Benchmark]
public IList<DateTime> DeserializeDateTimeList()
{
return JsonConvert.DeserializeObject<IList<DateTime>>(DateArrayJson);
}

[Benchmark]
public bool TryParseDateTimeOffsetIso()
{
bool success = true;
for (var index = 0; index < DateTimeStringReferences.Length; index++)
{
success &= DateTimeUtils.TryParseDateTimeOffsetIso(DateTimeStringReferences[index], out DateTimeOffset result);
ParsedDateTimesArray[index] = result;
}
return success;
}
}
}

Expand Down
18 changes: 5 additions & 13 deletions Src/Newtonsoft.Json.Tests/Benchmarks/SerializeBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,19 @@

#if HAVE_BENCHMARKS

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using BenchmarkDotNet.Attributes;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Tests.TestObjects;

namespace Newtonsoft.Json.Tests.Benchmarks
{
public class SerializeBenchmarks
{
private static readonly IList<RootObject> LargeCollection;
private static readonly MemoryStream ReusedMemoryStream = new();
private static readonly StreamWriter JsonFile = new(ReusedMemoryStream);
private static readonly JsonSerializer Serializer = new() { Formatting = Formatting.Indented };

static SerializeBenchmarks()
{
Expand All @@ -53,12 +49,8 @@ static SerializeBenchmarks()
[Benchmark]
public void SerializeLargeJsonFile()
{
using (StreamWriter file = System.IO.File.CreateText(TestFixtureBase.ResolvePath("largewrite.json")))
{
JsonSerializer serializer = new JsonSerializer();
serializer.Formatting = Formatting.Indented;
serializer.Serialize(file, LargeCollection);
}
ReusedMemoryStream.Seek(0, SeekOrigin.Begin);
Serializer.Serialize(JsonFile, LargeCollection);
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions Src/Newtonsoft.Json.Tests/Issues/Issue2768.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,15 @@ public void Test_Deserialize()
{
decimal d = JsonConvert.DeserializeObject<decimal>("0.0");

Assert.AreEqual("0.0", d.ToString());
Assert.AreEqual("0.0", d.ToString(CultureInfo.InvariantCulture));
}

[Test]
public void Test_Deserialize_Negative()
{
decimal d = JsonConvert.DeserializeObject<decimal>("-0.0");

Assert.AreEqual("0.0", d.ToString());
Assert.AreEqual("0.0", d.ToString(CultureInfo.InvariantCulture));
}

[Test]
Expand All @@ -95,7 +95,7 @@ public void Test_Deserialize_MultipleTrailingZeroes()
{
decimal d = JsonConvert.DeserializeObject<decimal>("0.00");

Assert.AreEqual("0.00", d.ToString());
Assert.AreEqual("0.00", d.ToString(CultureInfo.InvariantCulture));
}

[Test]
Expand Down Expand Up @@ -127,7 +127,7 @@ public void ParseJsonDecimal()
}
}

Assert.AreEqual("0.0", parsedValue.ToString());
Assert.AreEqual("0.0", parsedValue?.ToString(CultureInfo.InvariantCulture));
}

[Test]
Expand Down
10 changes: 5 additions & 5 deletions Src/Newtonsoft.Json.Tests/JsonConvertTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ public void WriteDateTime()
Assert.AreEqual(@"\/Date(253402300799999)\/", result.MsDateUtc);

DateTime year2000local = new DateTime(2000, 1, 1, 1, 1, 1, DateTimeKind.Local);
string localToUtcDate = year2000local.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFK");
string localToUtcDate = year2000local.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture);

result = TestDateTime("DateTime Local", year2000local);
Assert.AreEqual("2000-01-01T01:01:01" + GetOffset(year2000local, DateFormatHandling.IsoDateFormat), result.IsoDateRoundtrip);
Expand All @@ -791,7 +791,7 @@ public void WriteDateTime()
Assert.AreEqual(@"\/Date(" + DateTimeUtils.ConvertDateTimeToJavaScriptTicks(year2000local) + @")\/", result.MsDateUtc);

DateTime millisecondsLocal = new DateTime(2000, 1, 1, 1, 1, 1, 999, DateTimeKind.Local);
localToUtcDate = millisecondsLocal.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFK");
localToUtcDate = millisecondsLocal.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture);

result = TestDateTime("DateTime Local with milliseconds", millisecondsLocal);
Assert.AreEqual("2000-01-01T01:01:01.999" + GetOffset(millisecondsLocal, DateFormatHandling.IsoDateFormat), result.IsoDateRoundtrip);
Expand All @@ -804,7 +804,7 @@ public void WriteDateTime()
Assert.AreEqual(@"\/Date(" + DateTimeUtils.ConvertDateTimeToJavaScriptTicks(millisecondsLocal) + @")\/", result.MsDateUtc);

DateTime ticksLocal = new DateTime(636556897826822481, DateTimeKind.Local);
localToUtcDate = ticksLocal.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFK");
localToUtcDate = ticksLocal.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.FFFFFFFK", CultureInfo.InvariantCulture);

result = TestDateTime("DateTime Local with ticks", ticksLocal);
Assert.AreEqual("2018-03-03T16:03:02.6822481" + GetOffset(ticksLocal, DateFormatHandling.IsoDateFormat), result.IsoDateRoundtrip);
Expand All @@ -829,7 +829,7 @@ public void WriteDateTime()
Assert.AreEqual(@"\/Date(" + DateTimeUtils.ConvertDateTimeToJavaScriptTicks(year2000Unspecified.ToLocalTime()) + @")\/", result.MsDateUtc);

DateTime year2000Utc = new DateTime(2000, 1, 1, 1, 1, 1, DateTimeKind.Utc);
string utcTolocalDate = year2000Utc.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:ss");
string utcTolocalDate = year2000Utc.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);

result = TestDateTime("DateTime Utc", year2000Utc);
Assert.AreEqual("2000-01-01T01:01:01Z", result.IsoDateRoundtrip);
Expand All @@ -842,7 +842,7 @@ public void WriteDateTime()
Assert.AreEqual(@"\/Date(946688461000)\/", result.MsDateUtc);

DateTime unixEpoc = new DateTime(621355968000000000, DateTimeKind.Utc);
utcTolocalDate = unixEpoc.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:ss");
utcTolocalDate = unixEpoc.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:ss", CultureInfo.InvariantCulture);

result = TestDateTime("DateTime Unix Epoc", unixEpoc);
Assert.AreEqual("1970-01-01T00:00:00Z", result.IsoDateRoundtrip);
Expand Down
35 changes: 19 additions & 16 deletions Src/Newtonsoft.Json/Utilities/DateTimeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@
#endregion

using System;
using System.Runtime.InteropServices;

namespace Newtonsoft.Json.Utilities
{
internal enum ParserTimeZone
internal enum ParserTimeZone : byte
{
Unspecified = 0,
Utc = 1,
LocalWestOfUtc = 2,
LocalEastOfUtc = 3
}

[StructLayout(LayoutKind.Auto)]
internal struct DateTimeParser
{
static DateTimeParser()
Expand All @@ -56,15 +58,15 @@ static DateTimeParser()
Lz_zz = "-zz".Length;
}

public int Year;
public int Month;
public int Day;
public int Hour;
public int Minute;
public int Second;
public ushort Year;
public byte Month;
public byte Day;
public byte Hour;
public byte Minute;
public byte Second;
public int Fraction;
public int ZoneHour;
public int ZoneMinute;
public byte ZoneHour;
public byte ZoneMinute;
public ParserTimeZone Zone;

private char[] _text;
Expand Down Expand Up @@ -130,7 +132,7 @@ private bool ParseTime(ref int start)
&& ParseChar(start + LzHH_mm, ':')
&& Parse2Digit(start + LzHH_mm_, out Second)
&& Second < 60
&& (Hour != 24 || (Minute == 0 && Second == 0)))) // hour can be 24 if minute/second is zero)
&& ((Minute == 0 && Second == 0) || Hour != 24))) // hour can be 24 if minute/second is zero)
{
return false;
}
Expand Down Expand Up @@ -231,7 +233,7 @@ private bool ParseZone(int start)
return (start == _end);
}

private bool Parse4Digit(int start, out int num)
private bool Parse4Digit(int start, out ushort num)
{
if (start + 3 < _end)
{
Expand All @@ -244,24 +246,25 @@ private bool Parse4Digit(int start, out int num)
&& 0 <= digit3 && digit3 < 10
&& 0 <= digit4 && digit4 < 10)
{
num = (((((digit1 * 10) + digit2) * 10) + digit3) * 10) + digit4;
num = (ushort)((((((digit1 * 10) + digit2) * 10) + digit3) * 10) + digit4);
return true;
}
}
num = 0;
return false;
}

private bool Parse2Digit(int start, out int num)
private bool Parse2Digit(int start, out byte num)
{
if (start + 1 < _end)
int end = start + 1;
if (end < _end)
{
int digit1 = _text[start] - '0';
int digit2 = _text[start + 1] - '0';
int digit2 = _text[end] - '0';
if (0 <= digit1 && digit1 < 10
&& 0 <= digit2 && digit2 < 10)
{
num = (digit1 * 10) + digit2;
num = (byte)((digit1 * 10) + digit2);
return true;
}
}
Expand Down