Skip to content

Simplify trimming non significant digits in JsonUtf8Writer - #51367

Merged
eiriktsarpalis merged 3 commits into
dotnet:mainfrom
devsko:json-trim-opt
Apr 19, 2021
Merged

Simplify trimming non significant digits in JsonUtf8Writer#51367
eiriktsarpalis merged 3 commits into
dotnet:mainfrom
devsko:json-trim-opt

Conversation

@devsko

Copy link
Copy Markdown
Contributor

Found this while thinking about DateTime only.

Benchmark
privatestaticbyte[]data;[Params(true,false)]publicboolWithOffset;[Params(0,3,7)]publicintSignificantDigits;[GlobalSetup]publicvoidSetup(){stringstr=SignificantDigitsswitch{0=>"2019-04-24T14:50:17.0000000",3=>"2019-04-24T14:50:17.1230000",7=>"2019-04-24T14:50:17.1234567",
_ =>thrownewException()};data=Encoding.UTF8.GetBytes(str+(WithOffset?"+12:34":""));}[Benchmark]publicvoidUnrolled()=>TrimDateTimeOffset_Unrolled(data,out_);[Benchmark(Baseline=true)]publicvoidOriginal()=>TrimDateTimeOffset_Original(data,out_);
MethodWithOffsetSignificantDigitsMeanErrorStdDevRatio
UnrolledFalse06.490 ns0.1511 ns0.1413 ns0.77
OriginalFalse08.448 ns0.0376 ns0.0333 ns1.00
UnrolledFalse36.659 ns0.0242 ns0.0227 ns0.19
OriginalFalse334.686 ns0.0985 ns0.0873 ns1.00
UnrolledFalse75.318 ns0.0303 ns0.0253 ns0.14
OriginalFalse737.438 ns0.1090 ns0.0966 ns1.00
UnrolledTrue04.971 ns0.0953 ns0.0845 ns0.11
OriginalTrue043.909 ns0.1322 ns0.1236 ns1.00
UnrolledTrue35.321 ns0.0203 ns0.0190 ns0.12
OriginalTrue343.836 ns0.1169 ns0.0976 ns1.00
UnrolledTrue74.945 ns0.0288 ns0.0269 ns0.11
OriginalTrue744.322 ns0.1316 ns0.1167 ns1.00

@ghost

Copy link
Copy Markdown

Tagging subscribers to this area: @eiriktsarpalis, @layomia
See info in area-owners.md if you want to be subscribed.

Issue Details

Found this while thinking about DateTime only.

Benchmark
privatestaticbyte[]data;[Params(true,false)]publicboolWithOffset;[Params(0,3,7)]publicintSignificantDigits;[GlobalSetup]publicvoidSetup(){stringstr=SignificantDigitsswitch{0=>"2019-04-24T14:50:17.0000000",3=>"2019-04-24T14:50:17.1230000",7=>"2019-04-24T14:50:17.1234567",
_ =>thrownewException()};data=Encoding.UTF8.GetBytes(str+(WithOffset?"+12:34":""));}[Benchmark]publicvoidUnrolled()=>TrimDateTimeOffset_Unrolled(data,out_);[Benchmark(Baseline=true)]publicvoidOriginal()=>TrimDateTimeOffset_Original(data,out_);
MethodWithOffsetSignificantDigitsMeanErrorStdDevRatio
UnrolledFalse06.490 ns0.1511 ns0.1413 ns0.77
OriginalFalse08.448 ns0.0376 ns0.0333 ns1.00
UnrolledFalse36.659 ns0.0242 ns0.0227 ns0.19
OriginalFalse334.686 ns0.0985 ns0.0873 ns1.00
UnrolledFalse75.318 ns0.0303 ns0.0253 ns0.14
OriginalFalse737.438 ns0.1090 ns0.0966 ns1.00
UnrolledTrue04.971 ns0.0953 ns0.0845 ns0.11
OriginalTrue043.909 ns0.1322 ns0.1236 ns1.00
UnrolledTrue35.321 ns0.0203 ns0.0190 ns0.12
OriginalTrue343.836 ns0.1169 ns0.0976 ns1.00
UnrolledTrue74.945 ns0.0288 ns0.0269 ns0.11
OriginalTrue744.322 ns0.1316 ns0.1167 ns1.00
Author:devsko
Assignees:-
Labels:

area-System.Text.Json

Milestone:-


// Find the position after the last significant digit in seconds fraction.
int curIndex;
if (buffer[maxLenNoOffset - 1] == '0')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no question that the existing implementation is doing unnecessary work, but that being said I do find the formatting of this particular section to be slightly disagreeable. This is not a hot path method so I'm not sure it's worth unrolling. I'd be interested to see performance numbers with your curIndex calculation logic refactored into a loop.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop version is only slightly slower. I would say this path can get pretty hot depending on the payload. But I agree - will change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@devskodevsko left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @eiriktsarpalis. Unfortunately the fastest loop version i could come up with still regresses (about 1.4x) compared to the original version when there are no significant digits and no offset. In this case the original version is completely branchless. In all other cases the loop is much faster but i consider this the most common real-world scenario. Because this method runs for every single DateTime(Offset) that gets serialized, i suggest to either go with the unrolled version or close this PR and leave the trimming as it is.

MethodWithOffsetSignificantDigitsMeanErrorStdDevRatioRatioSD
LoopFalse010.402 ns0.1525 ns0.1427 ns1.450.02
UnrolledFalse05.099 ns0.1284 ns0.1138 ns0.710.02
OriginalFalse07.204 ns0.0210 ns0.0175 ns1.000.00
LoopFalse38.579 ns0.0430 ns0.0402 ns0.240.00
UnrolledFalse35.645 ns0.0219 ns0.0205 ns0.160.00
OriginalFalse335.762 ns0.0807 ns0.0674 ns1.000.00
LoopFalse75.182 ns0.0274 ns0.0256 ns0.130.00
UnrolledFalse73.558 ns0.0093 ns0.0077 ns0.090.00
OriginalFalse738.781 ns0.1142 ns0.1012 ns1.000.00
LoopTrue010.795 ns0.0179 ns0.0150 ns0.240.00
UnrolledTrue03.606 ns0.0152 ns0.0135 ns0.080.00
OriginalTrue044.069 ns0.0850 ns0.0710 ns1.000.00
LoopTrue310.793 ns0.0128 ns0.0114 ns0.240.00
UnrolledTrue33.602 ns0.0175 ns0.0164 ns0.080.00
OriginalTrue344.173 ns0.2023 ns0.1892 ns1.000.00
LoopTrue710.845 ns0.0475 ns0.0444 ns0.250.00
UnrolledTrue73.988 ns0.0345 ns0.0322 ns0.090.00
OriginalTrue744.011 ns0.0688 ns0.0610 ns1.000.00
Loop version
publicstaticvoidTrimDateTimeOffset(Span<byte>buffer,outintbytesWritten){// Assert buffer is the right length for:// YYYY-MM-DDThh:mm:ss.fffffff (JsonConstants.MaximumFormatDateTimeLength)// YYYY-MM-DDThh:mm:ss.fffffffZ (JsonConstants.MaximumFormatDateTimeLength + 1)// YYYY-MM-DDThh:mm:ss.fffffff(+|-)hh:mm (JsonConstants.MaximumFormatDateTimeOffsetLength)Debug.Assert(buffer.Length==JsonConstants.MaximumFormatDateTimeLength||buffer.Length==JsonConstants.MaximumFormatDateTimeLength+1||buffer.Length==JsonConstants.MaximumFormatDateTimeOffsetLength);// Find the position after the last significant digit in seconds fraction.intcurIndex;for(curIndex=JsonConstants.MaximumFormatDateTimeLength-1;buffer[curIndex]=='0';curIndex--);if(curIndex>=JsonConstants.MaximumFormatDateTimeLength-7){curIndex++;}// We are either trimming a// (a) DateTimeOffset, or a// DateTime with// (b) DateTimeKind.Local or// (c) DateTimeKind.Utcif(buffer.Length==JsonConstants.MaximumFormatDateTimeLength){// (b) There is no offset to copy.bytesWritten=curIndex;}elseif(buffer.Length==JsonConstants.MaximumFormatDateTimeOffsetLength){// (a) We have a non-UTC offset i.e. (+|-)hh:mm that are always 6 characters to copy.buffer[curIndex]=buffer[JsonConstants.MaximumFormatDateTimeLength];buffer[curIndex+1]=buffer[JsonConstants.MaximumFormatDateTimeLength+1];buffer[curIndex+2]=buffer[JsonConstants.MaximumFormatDateTimeLength+2];buffer[curIndex+3]=buffer[JsonConstants.MaximumFormatDateTimeLength+3];buffer[curIndex+4]=buffer[JsonConstants.MaximumFormatDateTimeLength+4];buffer[curIndex+5]=buffer[JsonConstants.MaximumFormatDateTimeLength+5];bytesWritten=curIndex+6;}else{// (c) There is a single 'Z'. Just write it at the current index.Debug.Assert(buffer[JsonConstants.MaximumFormatDateTimeLength+1-1]=='Z');buffer[curIndex]=(byte)'Z';bytesWritten=curIndex+1;}}

@eiriktsarpalis

Copy link
Copy Markdown
Member

Thanks!

@eiriktsarpalis
eiriktsarpalis merged commit 9b364cf into dotnet:mainApr 19, 2021
@devsko
devsko deleted the json-trim-opt branch April 19, 2021 10:35
@ghostghost locked as resolved and limited conversation to collaborators May 19, 2021
@karelzkarelz added this to the 6.0.0 milestone May 20, 2021
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@devsko@eiriktsarpalis@karelz