Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Add IPAddress Span-based APIs #23224

Merged
merged 2 commits into from
Aug 21, 2017
Merged

Add IPAddress Span-based APIs #23224

merged 2 commits into from
Aug 21, 2017

Conversation

Paxxi
Copy link

@Paxxi Paxxi commented Aug 14, 2017

resolves #22607
This adds

public IPAddress(ReadOnlySpan<byte> address)
public IPAddress(ReadOnlySpan<byte> address, long scopeid)
public bool TryWriteBytes(Span<byte> destination, out int bytesWritten)
public static IPAddress Parse(ReadOnlySpan<char> ipSpan)
public static bool TryParse(ReadOnlySpan<char> ipSpan, out IPAddress address)
public bool TryFormat(Span<char> destination, out int bytesWritten)

Assumption is that the use of ReadOnlySpan or Span overloads are by the most demanding use cases so where it makes sense the existing methods call the Span related methods or the internal helper methods have been converted to use spans.

@stephentoub I think this is ready for review now

@@ -150,6 +150,30 @@ public IPAddress(byte[] address, long scopeid)
PrivateScopeId = (uint)scopeid;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can the IPAddress(byte[], long) ctor be implemented in terms of this new one?

public IPAddress(byte[] address, long scopeid) :
    this(new ReadOnlySpan<byte>(address ?? new ArgumentNullException(nameof(address))), scopeid)
{
}

or does that negatively impact the perf of the byte[]-based ctor?

Copy link
Author

Choose a reason for hiding this comment

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

I didn't think of throwing the exception that way. Started with sharing the code but then the null check had to be specifically in the array based ctor and I ended up with the copy pasting of code.

Will change both ctors to use this type of null check. Thanks!

}
}

public IPAddress(ReadOnlySpan<byte> address)
Copy link
Member

Choose a reason for hiding this comment

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

Same question; is there a reason we can't implement the existing byte[]-based ctor in terms of the span-based one?

{
address = null;
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Why is this check needed?

Copy link
Author

Choose a reason for hiding this comment

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

Leftover from troubleshooting that snuck in. I'll get rid of it.

return false;
}

address = IPAddressParser.Parse(ipSpan, true);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: I know the existing code doesn't have this, but it'd be nice to name the Boolean argument at the call site; otherwise it's difficult to know while reading the code what it means. (Also the parens around the expression below are unnecessary... again, I realize you were just copying the existing style.)

}

TryWriteBytes(new Span<byte>(bytes), out int bytesWritten);
Copy link
Member

Choose a reason for hiding this comment

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

Nit: for robustness, it'd be good to assert that this returns true, e.g.

bool success = TryWriteBytes(new Span<byte>(bytes), out int bytesWritten);
Debug.Assert(success);
return bytes;

bytes[1] = (byte)(address >> 8);
bytes[2] = (byte)(address >> 16);
bytes[3] = (byte)(address >> 24);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This code could be cleaned up while you're at it, and simplified to just:

int length = IsIPv6 ? IPAddressParserStatics.IPv6AddressBytes : IPAddressParserStatics.IPv4AddressBytes;
var bytes = new byte[length];

}

return IPAddressParser.IPv6AddressToString(_numbers, PrivateScopeId, destination, out charsWritten);
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: you might condense this slightly, e.g.

public bool TryFormat(Span<char> destination, out int charsWritten) =>
    IsIPv4 ?
        IPAddressParser.IPv4AddressToString(PrivateAddress, destination, out charsWritten) :
        IPAddressParser.IPv6AddressToString(_numbers, PrivateScopeId, destination, out charsWritten);


}

internal static unsafe IPAddress Parse(char* ipStringPtr, int ipStringPtrLength, bool tryParse)
Copy link
Member

Choose a reason for hiding this comment

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

Could this be written in terms of ReadOnlySpan<char> rather than in terms of char*? That'd a) be safer and b) avoid pinning for unnecessary periods of time. Regardless, the null check below can be moved up into the string-based overload.

}

if (ipString.IndexOf(':') != -1)
if (ContainsColon(ipStringPtr, ipStringPtrLength))
Copy link
Member

Choose a reason for hiding this comment

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

If you do switch to using span instead of pointers, you shouldn't need this custom ContainsColon either, as you should be able to just use Span.IndexOf. At the moment that'll require a reference to System.Memory, but hopefully that won't be needed in the near future.

Copy link
Author

Choose a reason for hiding this comment

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

Any idea of which way to go? Both will require changes once the reference isn't required anymore.

Copy link
Member

Choose a reason for hiding this comment

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

Especially in networking-related code, we want to avoid unnecessary unsafe code, so it'd be great to go the route of using spans as much as possible rather than introducing additional pointer-based code. For now I'd minimize changing pointer-based code to spans, as it could have more perf impact right now than we want to accept, but when converting string-based methods to something else, let's prefer spans over pointers.

{
charsWritten = 0;
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Seems like this method should be made a private helper, and then another internal wrapper for it should be added that does this check. It's not needed when called from the string-returning version, only for the span one.

Copy link
Author

Choose a reason for hiding this comment

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

Is it worth the extra method call to avoid that branch instruction? I think it might be better this way but I'm open to trying to measure it.

Debug.Assert(address != null);
Debug.Assert(address.Length == IPAddressParserStatics.IPv6AddressShorts);

var buffer = Ipv6AddressToStringHelper(address, scopeId);
Copy link
Member

Choose a reason for hiding this comment

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

Why build up into a StringBuilder only to then copy the data out? We can't just go into the destination span directly?

Copy link
Author

Choose a reason for hiding this comment

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

I went for this design to keep as much as possible shared. I will look into allocating a fixed size buffer for the string version instead.

Copy link
Member

Choose a reason for hiding this comment

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

I see: the existing code is already using a StringBuilder, and you're now just passing that out of the call. Ok, makes sense. After this PR, it might make sense to look at cleaning that up further; seems like we should be able to avoid the StringBuilder entirely, but for now it's fine.

Nit: please change var => StringBuilder.

Copy link
Author

Choose a reason for hiding this comment

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

Should I avoid using var in general for corefx repo?

Copy link
Member

Choose a reason for hiding this comment

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

No, var is ok, but only if the type is named on the right-hand side, i.e. with a new or cast expression. We want the type of the variable to be easily understood by someone reading the code.

do
{
int rem;
number = Math.DivRem(number, 10, out rem);
addressString[--i] = (char)('0' + rem);
addressString[offset - ++i] = (char)('0' + rem);
Copy link
Member

Choose a reason for hiding this comment

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

This is now accessing a ref inside of a loop. That's potentially expensive.

Copy link
Author

Choose a reason for hiding this comment

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

Didn't know that about refs. Will fix.

Copy link
Member

Choose a reason for hiding this comment

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

It just inhibits certain optimizations, as the compiler doesn't necessarily know whether the target is changing between uses.

Copy link
Author

Choose a reason for hiding this comment

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

Using a local copy instead.

} while (number != 0);
offset = i;
Copy link
Member

Choose a reason for hiding this comment

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

I'm not understanding why this function needed to change...?

Copy link
Author

Choose a reason for hiding this comment

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

Previous version wrote the IP from the end of the buffer. Writing this directly into the span caused leading \0. Changed it to write from the start of the buffer.

Copy link
Member

Choose a reason for hiding this comment

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

I see. Ok. In that case it'd be worth adding an assert at the beginning validating that number <= 255. Or better yet, make it a byte rather than an int.

Copy link
Author

Choose a reason for hiding this comment

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

Went with the assert as Math.DivRem only have overloads for int and long

}

public static unsafe bool Ipv4StringToAddress(string ipString, out long address)
public static unsafe bool Ipv4StringToAddress(char* ipStringPtr, int ipStringPtrLength, out long address)
Copy link
Member

Choose a reason for hiding this comment

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

As earlier, it'd be good to using ReadOnlySpan<char> instead of the original string, rather than substituting char*. Especially in this IPAddress code, it'd be great to try to avoid adding any unnecessary unsafe code.

@@ -15,7 +15,7 @@ internal static class IPv4AddressHelper
private const int NumberOfLabels = 4;

// Only called from the IPv6Helper, only parse the canonical format
internal static unsafe int ParseHostNumber(string str, int start, int end)
internal static unsafe int ParseHostNumber(char* str, int start, int end)
Copy link
Member

@stephentoub stephentoub Aug 14, 2017

Choose a reason for hiding this comment

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

Ditto on using ReadOnlySpan<char> unless there's a strong reason to avoid it. Will stop commenting on this, but it applies throughout the code you're touching... let's try to only switch to char* when calling existing functions that accept it.


IPAddress ipAddress;
Assert.False(IPAddress.TryParse(null, out ipAddress));
Assert.False(IPAddress.TryParse(test, out ipAddress));
Copy link
Member

Choose a reason for hiding this comment

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

What was wrong with the code in this file as it was?

Copy link
Author

Choose a reason for hiding this comment

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

The previous behaviour failed to build with ambigous overload error. Forcing the null to be of string type eliminates the Span version from the available candidates.

Copy link
Member

Choose a reason for hiding this comment

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

Ok. FWIW, you can do that just by adding a cast, e.g. (string)null.


namespace System.Net.Primitives.Functional.Tests
{
public class IPAddressParsingSpan
Copy link
Member

Choose a reason for hiding this comment

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

It's great to see lots of tests added, but a lot of these look like duplication of existing tests, just for the span-based overloads instead of the array-based ones. It'd be good to find a way to avoid the duplication and reuse tests once for both the array-based and span-based APIs. There are a variety of approaches to this, but one example is here:
https://github.com/dotnet/corefx/blob/master/src/Common/tests/System/Security/Cryptography/AlgorithmImplementations/RSA/EncryptDecrypt.cs
The tests are all in an abstract base class that exposes virtuals for each of the APIs that has an array vs span variant, and then derived types just override those methods, providing functionality to the base class to be tested.

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Thanks for working on this!

@Paxxi
Copy link
Author

Paxxi commented Aug 15, 2017

Thanks for the thorough review and especially the nitpicks, good way to learn the project coding style.

Will go over the internal methods again and see about using Spans instead of the char* versions.
Same goes for the duplication of the tests. Will try to at least merge the test data.

@Paxxi
Copy link
Author

Paxxi commented Aug 16, 2017

Benchmarked ctor changes with these results

With duplicated code.

Test Name Metric Iterations AVERAGE STDEV.S MIN MAX
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 0.002 0.002 7.331E-004 0.063
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [143, 24, 20, 36]) Duration 1000 0.001 9.633E-004 3.666E-004 0.020
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [16, 32, 48, 64, 80, ...]) Duration 1000 0.002 7.616E-004 3.666E-004 0.010
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) Duration 1000 7.962E-004 5.741E-004 3.666E-004 0.011
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 8.277E-004 3.463E-004 3.666E-004 0.004
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [143, 24, 20, 36]) Duration 1000 6.375E-004 2.918E-004 3.666E-004 0.003
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 9.406E-004 6.106E-004 3.666E-004 0.008
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000

With the chained constructor

Test Name Metric Iterations AVERAGE STDEV.S MIN MAX
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [143, 24, 20, 36]) Duration 1000 0.001 9.332E-004 3.666E-004 0.013
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 0.002 0.003 3.666E-004 0.046
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [143, 24, 20, 36]) Duration 1000 8.394E-004 6.159E-004 3.666E-004 0.008
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [16, 32, 48, 64, 80, ...]) Duration 1000 8.028E-004 5.026E-004 3.666E-004 0.009
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) Duration 1000 9.116E-004 5.279E-004 3.666E-004 0.007
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 8.167E-004 5.740E-004 3.666E-004 0.007
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [143, 24, 20, 36]) Duration 1000 7.580E-004 3.983E-004 3.666E-004 0.004
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 0.001 7.585E-004 3.666E-004 0.010
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000

I haven't looked at replacing the StringBuilder yet.

}

const bool tryParse = true;
address = IPAddressParser.Parse(ipString, tryParse);
Copy link
Member

Choose a reason for hiding this comment

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

I should have been clearer about naming the argument. I meant doing it like this:

address = IPAddressParser.Parse(ipString, tryParse: true)

public static bool TryParse(ReadOnlySpan<char> ipSpan, out IPAddress address)
{
const bool tryParse = true;
address = IPAddressParser.Parse(ipSpan, tryParse);
Copy link
Member

Choose a reason for hiding this comment

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

Ditto.

}

const bool tryParse = false;
return IPAddressParser.Parse(ipString, tryParse);
Copy link
Member

Choose a reason for hiding this comment

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

Ditto.

}

return false;
}
Copy link
Member

@stephentoub stephentoub Aug 16, 2017

Choose a reason for hiding this comment

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

Rather than writing this method, you should be able to add a reference to System.Memory in the .csproj:
https://github.com/dotnet/corefx/blob/master/src/System.Net.Primitives/src/System.Net.Primitives.csproj#L200
Then you can delete this method, and at the call site instead of doing:

if (ContainsColon(ipSpan))

do:

if (ipSpan.IndexOf(':') >= 0)

That not only saves the extra code, but this will benefit from any perf improvements over the standard loop done in IndexOf.

@@ -21,22 +34,34 @@ internal static unsafe IPAddress Parse(string ipString, bool tryParse)
}
throw new ArgumentNullException(nameof(ipString));
}

return Parse(ipString.ToCharArray(), tryParse);
Copy link
Member

Choose a reason for hiding this comment

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

Ouch, we should not be allocating a char[] from the string. Once you've added the System.Memory reference, you can use the AsReadOnlySpan extension method.

}

return false;
}
internal static unsafe IPAddress Parse(string ipString, bool tryParse)
Copy link
Member

Choose a reason for hiding this comment

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

This no longer needs to be unsafe.

if (ipString.IndexOf(':') != -1)
internal static unsafe IPAddress Parse(ReadOnlySpan<char> ipSpan, bool tryParse)
{
return Parse(ipSpan, tryParse);
Copy link
Member

Choose a reason for hiding this comment

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

Is this a typo? Won't this call itself recursively and stack overflow?

Copy link
Author

Choose a reason for hiding this comment

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

This seems to have worked by accident because of the parameters. Renaming it to avoid issues.


internal static unsafe IPAddress Parse(ReadOnlySpan<char> ipSpan, int ipStringPtrLength, bool tryParse)
{

Copy link
Member

Choose a reason for hiding this comment

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

Nit: unnecessary blank line

return true;
}

internal static unsafe int IPv4AddressToStringHelper(uint address, char* addressString)
Copy link
Member

Choose a reason for hiding this comment

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

This no longer needs to use a pointer. The pointer was here due to stackallocating, but now we can just write this in terms of Span<char>, making this method safer against possible overflow bugs. The span-based caller can just pass in the target span. And the string-based caller can still stackalloc the target space, but then rather than passing that in directly, it can create a span from it and pass in the span here. That then also avoids the pinning that's been added in the span-based caller above.


Assert.False(ip.TryFormat(new Span<char>(result), out int charsWritten));
Assert.Equal(0, charsWritten);
}
Copy link
Member

Choose a reason for hiding this comment

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

Could we add an assert that verifies nothing was written to the output array? e.g.

Assert.Equal<byte>(new char[result.Length], result);


[Theory]
[MemberData(nameof(ValidIpv4Addresses))]
public void TryFormatIPv4_ValidAddress_Failure(string address, string expected)
Copy link
Member

Choose a reason for hiding this comment

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

Why would valid addresses fail to format? Too little destination space? If so, can we include that in the title?

Copy link
Member

Choose a reason for hiding this comment

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

Assuming this is the case, my suggestion would be to combine the success and failure tests for this, and validate three things:

  • That it succeeds with exactly the right amount of output space
  • That it fails with one less byte available than is needed, and none of the output is written
  • That it succeeds with one more byte available than is needed, and that extra byte remains unwritten


return true;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

You could do a similar thing with:

Assert.Equal<byte>(
    expected.AsSpan().Slice(0, length).ToArray(),
    actual.AsSpan().Slice(0, length).ToArray());

addressString[--offset] = '.';
FormatIPv4AddressNumber((int)((address >> 8) & 0xFF), addressString, ref offset);
addressString[--offset] = '.';
int charsWritten = IPv4AddressToStringHelper(address, new Span<char>((void*)addressString, MaxLength));
Copy link
Author

Choose a reason for hiding this comment

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

Is there a better way to do this than to cast to void*. Would having a T* overload for Span collide with the void* version?

Copy link
Member

Choose a reason for hiding this comment

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

You shouldn't need to explicitly cast: a char* is implicitly convertible to a void*.

Copy link
Member

Choose a reason for hiding this comment

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

ps The reason it's void* is because you can't have a T* in C#.

Copy link
Author

Choose a reason for hiding this comment

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

ahh, thanks!

@Paxxi
Copy link
Author

Paxxi commented Aug 18, 2017

I think it's better to separate the failure cases into their own tests. It makes it clearer what's failing when looking at the test results, I did rename the failure tests for clarity. I merged the right size/size+1 tests in IPAddressParsingSpan.

@@ -202,6 +202,7 @@
<Reference Include="System.Runtime.Extensions" />
<Reference Include="System.Runtime.InteropServices" />
<Reference Include="System.Threading" />
<Reference Include="System.Memory" />
Copy link
Member

Choose a reason for hiding this comment

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

Nit: please add it in alphabetical order

// Failed to parse the address.
address = 0;
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Does the pinning need to extend down to here? Seems like it only need to be done just for the ParseNonCanonical call, as it was in the old code.

Copy link
Author

Choose a reason for hiding this comment

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

I thought about it, reducing the fixed is a bit uglier with variable declarations so I went with this. Can change it to reduce the fixed instead.

{
using (iteration.StartMeasurement())
{
ip.TryWriteBytes(bytesSpan, out bytesWritten);
Copy link
Member

Choose a reason for hiding this comment

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

Is this doing enough work per iteration to produce meaningful and consistent results? Sometimes in these perf tests we need to do multiple calls / loop inside of the StartMeasurement using. Same question goes for the other tests, too.

Copy link
Author

Choose a reason for hiding this comment

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

You have some test data in this PR, the numbers are very small so possibly too little for meaningsful results. Isn't it possible to increase iteration counts per method?

Copy link
Member

Choose a reason for hiding this comment

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

Isn't it possible to increase iteration counts per method?

Yes, but there's overhead to the:

foreach (var iteration in Benchmark.Iterations)
{
     using (iteration.StartMeasurement())
     {
         ...
     }
}

If the work being measured is small compared to that, the perf test isn't effective. That's why you see code like this:
https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/Performance/Perf.DateTime.cs#L19-L21
and this:
https://github.com/dotnet/corefx/blob/master/src/System.Runtime/tests/Performance/Perf.String.netcoreapp.cs#L25-L26

Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

A few more comments/questions, but overall looks great. Thank you for working on this.

@stephentoub
Copy link
Member

@dotnet/dnceng, another connection failure:

curl: (7) Failed to connect to dotnetcli.azureedge.net port 443: Operation timed out
tar: Error opening archive: Failed to open '/Users/dotnet-bot/j/workspace/dotnet_corefx/master/osx-TGroup_netcoreapp+CGroup_Debug+AGroup_x64+TestOuter_false_prtest/Tools/dotnetcli/dotnet.tar'

Seems to be happening a lot.

@mmitche
Copy link
Member

mmitche commented Aug 18, 2017

@Paxxi
Copy link
Author

Paxxi commented Aug 18, 2017

Test Name Metric Iterations AVERAGE STDEV.S MIN MAX
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [143, 24, 20, 36]) Duration 1000 5.187 0.703 4.329 9.712
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [143, 24, 20, 36]) GC Count 1000 1.017 0.129 1.000 2.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 8.922 0.711 7.607 17.122
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 2.035 0.184 2.000 3.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [143, 24, 20, 36]) Duration 1000 3.648 0.675 3.017 6.954
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [143, 24, 20, 36]) GC Count 1000 1.017 0.129 1.000 2.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [16, 32, 48, 64, 80, ...]) Duration 1000 7.026 0.464 6.045 10.790
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Span(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 2.035 0.184 2.000 3.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) Duration 1000 4.982 0.711 4.185 9.020
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) GC Count 1000 0.610 0.488 0.000 1.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 9.170 1.899 7.469 38.875
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.764 0.425 0.000 1.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [143, 24, 20, 36]) Duration 1000 2.047 0.614 1.699 12.842
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [16, 32, 48, 64, 80, ...]) Duration 1000 2.152 1.159 1.688 23.875
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryFormat(address: [143, 24, 20, 36]) Duration 1000 2.431 0.117 2.209 2.962
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryFormat(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryFormat(address: [16, 32, 48, 64, 80, ...]) Duration 1000 8.089 0.720 7.185 14.943
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryFormat(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [143, 24, 20, 36]) Duration 1000 3.576 1.026 2.760 15.283
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 9.155 2.004 6.712 16.137
System.Net.Primitives.Tests.IPAddressPerformanceTests.TryWriteBytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000

Tweaked the work amount for the perf tests.

@stephentoub
Copy link
Member

Thanks for the fixes, @Paxxi. Any regressions in the perf tests for the existing methods?

@Paxxi
Copy link
Author

Paxxi commented Aug 18, 2017

Here's backported tests run without these changes

Test Name Metric Iterations AVERAGE STDEV.S MIN MAX
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [143, 24, 20, 36]) Duration 1000 2.583 0.220 2.340 4.755
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [143, 24, 20, 36]) GC Count 1000 1.017 0.129 1.000 2.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 5.718 1.061 5.218 31.317
System.Net.Primitives.Tests.IPAddressPerformanceTests.Ctor_Bytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 2.035 0.184 2.000 3.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) Duration 1000 2.227 0.700 1.927 17.972
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [143, 24, 20, 36]) GC Count 1000 0.611 0.488 0.000 1.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) Duration 1000 5.776 1.059 5.055 29.662
System.Net.Primitives.Tests.IPAddressPerformanceTests.GetAddressBytes(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.764 0.425 0.000 1.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [143, 24, 20, 36]) Duration 1000 1.756 0.147 1.644 2.619
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [143, 24, 20, 36]) GC Count 1000 0.000 0.000 0.000 0.000
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [16, 32, 48, 64, 80, ...]) Duration 1000 1.781 0.509 1.643 16.200
System.Net.Primitives.Tests.IPAddressPerformanceTests.ToString(address: [16, 32, 48, 64, 80, ...]) GC Count 1000 0.000 0.000 0.000 0.000

Quite large difference in total, but e.g. GetAddressBytes is 60 million method calls. I leave the decision to you as I'm not very familiar with what's considered an acceptable performance regression.

@stephentoub
Copy link
Member

Thanks. I'll pull down the PR and take a look.

Paxxi and others added 2 commits August 21, 2017 13:08
See issue #22607
This adds
public IPAddress(ReadOnlySpan<byte> address)
public IPAddress(ReadOnlySpan<byte> address, long scopeid)
public bool TryWriteBytes(Span<byte> destination, out int bytesWritten)
public static IPAddress Parse(ReadOnlySpan<char> ipSpan)
public static bool TryParse(ReadOnlySpan<char> ipSpan, out IPAddress address)
public static bool TryFormat(Span<char> destination, out int bytesWritten)
@stephentoub
Copy link
Member

Quite large difference in total, but e.g. GetAddressBytes is 60 million method calls.

I took a look at the four methods:

  • IPAddress(byte[]). There’s about a 20% difference in a microbenchmark. The main difference here is just that there's one more non-inlined method call than there was previously (the non-inlined IPAddress(byte[]) is calling the non-inlined IPAddress(ReadOnlySpan<byte>); the work involved in the ctor (especially for IPv4) is so small that such an additional call adds measurably to the overall cost. We can address that by reducing the size of the IPAddress(byte[]) ctor to the point where the JIT wants to inline it, by moving the constructing/throwing of the null exception into a non-inlined helper function. The result is still one non-inlined function (the span-based one instead of the byte[]-based one), and the regression goes away.
  • IPAddress.Parse. Similarly, a few layers of extra function calls was contributing measurably to the cost. When I remove those, the new code appears to be only ~1-2% slower than the current, and without having to duplicate tons of code. I think that’s acceptable for now, and I expect it’ll get a little better as the JIT improves around its handling of spans and some of the span helpers (e.g. IndexOf) improve; even if they don’t improve, each Parse on a value like “127.0.0.1” is currently taking only ~50ns on my machine, so a 1-2% impact is fairly inconsequential, and most of the time is spent working with char*s, so other than the IndexOf call, span vs char[] shouldn't change the per-char overheads.
  • IPAddress.ToString. There are actually two cases here, one that’s tested by the perf tests you added and one that’s not. The one that is tested is arguably the least important: IPAddress caches the generated string created by ToString (I don’t know why), so when accessing ToString on the same address in a loop, all that’s really being tested is how fast we can check for the field’s value and return it. The more interesting piece is how fast we can generate the string initially. And the regression here does appear to be from the code we changed from char* to Span<char>; putting it back to use char* fixes the regression.
  • IPAddress.GetAddressBytes. This had the largest regression, upwards of 3x. The issue here was just that we’d added a lot of unnecessary code to the array-based paths, due to layering it on top of the public TryWriteBytes. Unnecessary length checks, unnecessary branches, etc. Refactoring it to just share the logic for writing the bytes to the span removes most of the regression. There’s then still a small regression which is due to an extra non-inlined method call, which we can address just by aggressively inlining the code that had been inlined manually and is now factored out.

I’ve pushed a commit to address these issues.

@stephentoub
Copy link
Member

@dotnet-bot test Linux x64 Release Build please
@dotnet-bot test Windows x86 Release Build please

@stephentoub
Copy link
Member

Thanks for working on this, @Paxxi.

@stephentoub stephentoub merged commit 1fc5e1e into dotnet:master Aug 21, 2017
@Paxxi
Copy link
Author

Paxxi commented Aug 22, 2017

Thank you for the thorough explanation about the performance aspects! This has been a great experience and already looking for new tickets 😄

@karelz karelz modified the milestone: 2.1.0 Aug 28, 2017
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
Sign up for free to 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.

Add IPAddress Span-based APIs
5 participants