-
Notifications
You must be signed in to change notification settings - Fork 748
Use buffer pooling when calculating partition filters #4489
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
Comments
Perhaps stackalloc is overly ambitious on the net462 side. We could add the System.Memory package too to get Span<> support, but there's not really any APIs to target for it. |
@stevenaw I thought as you once and made a DotNetBenchmark for I'm not sure, but I think that filters are only called from a single thread as part of building the test suite. |
Oh that is a good idea about the single buffer thanks @manfred-brands ! I've done some tinkering to validate approach as a POC but nothing really substantial enough to assign this to myself. Absolutely, any change here should be accompanied by a benchmark |
I ran some benchmarks on net48 + net60 and I think the arraypool + oneshot hashing approach has promise. netfx
net60
There's still more gains to be found here, but I think it's worth pursuing at least on net6 +
public uint ComputeHashValue_ArrayPool()
{
var name = TestMethodName;
var encoding = Encoding.UTF8;
var bufferLength = encoding.GetMaxByteCount(name.Length);
var buffer = ArrayPool<byte>.Shared.Rent(bufferLength);
byte[] hashValue;
try
{
var bytesWritten = encoding.GetBytes(name, 0, name.Length, buffer, 0);
#if NETFRAMEWORK
// SHA256 ComputeHash will return 32 bytes, we will use the first 4 bytes of that to convert to an unsigned integer
using var hashAlgorithm = SHA256.Create();
hashValue = hashAlgorithm.ComputeHash(buffer, 0, bytesWritten);
#else
hashValue = SHA256.HashData(new Span<byte>(buffer, 0, bytesWritten));
#endif
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
return BitConverter.ToUInt32(hashValue, 0);
} |
As an exception from hashing should be quite unlikely, you could save some CPU cycles from not having a try-catch. |
If |
Thanks @lahma ! Indeed, some of your past contributions and the size of suites you've helped optimize Nunit for is partly what inspired me to look for optimizations here too |
@lhama |
@stevenaw Never change two parameters at once. |
Very true @manfred-brands |
As an aside, how important would thread safety be in this and other filters? While filters are currently applied sequentially, there's always the possibility a higher-level optimization could change that. For example, within this filter each test's partition is calculated independently meaning a parallel divide and conquer approach could also theoretically apply here for very large suites. There would, of course, be ways we could work around any lack of thread safety but it still may be something worth considering too |
Indeed a future TestSuiteBuilder could build fixtures/tests in parallel. For the .net core build, I suggest to use stackalloc and the static hash method. |
The partition filter currently generates a new byte array for each method name. Given partitioning is most likely to be helpful (and thus most likely to be used) when splitting up a large suite, there's the potential for this to allocate quite a few times.
https://github.com/nunit/nunit/blob/master/src/NUnitFramework/framework/Internal/Filters/PartitionFilter.cs#L110
It might be worth looking into using
ArrayPool<byte>
or a single large buffer at instance level here instead of making a new one each time. The one-shot hashing helpers and stackalloc could be helpful here too on the .NET6 build.I think this should be possible for all library targets but may require taking a dependency on System.Buffers on the net462 build if ArrayPool is used.
@nunit/framework-team thoughts or objections (particularly about the dependency this would add)?
The text was updated successfully, but these errors were encountered: