dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.82k stars 4.61k forks source link

MemoryExtensions.SequenceCompare #20525

Closed AronParker closed 4 years ago

AronParker commented 7 years ago

Latest Proposal

See https://github.com/dotnet/corefx/issues/16878#issuecomment-349414287

Problem:

As of right now there is no function to efficiently compare memory. The result is that such comparisons are usually done on a byte by byte basis or even worse Enumerable.SequenceEqual, which performs really slow.

Suggested APIs:

class static SpanExtensions
{
    static int SequenceCompareTo(this ReadOnlySpan<byte> first, ReadOnlySpan<byte> second);
}

Design

It would fit along nicely with other memory functions defined in Buffer such as memcpy or memmove. The arrays would have to be primitives and can be efficiently compared.

Implementation

Different implementations can be used for different lengths. Here are some examples:

The individual thresholds would have to be profiled of course.

Original Proposal

class static SpanExtensions
{
    static int SequenceCompare(this ReadOnlySpan<byte> first, ReadOnlySpan<byte> second);
}

[EDIT] Add C# syntax highlight for code by @karelz

mellinoe commented 7 years ago

This may be a helpful addition, but I don't think that using Array in new API's is a good idea, generally speaking. More likely we would want overloads taking T[], and perhaps void*, length ?

karelz commented 7 years ago

Might be a good idea for Span?

AronParker commented 7 years ago

This may be a helpful addition, but I don't think that using Array in new API's is a good idea, generally speaking. More likely we would want overloads taking T[], and perhaps void*, length ?

True, I've chosen Array in order to make it analog to Buffer.BlockCopy(Array,int,Array,int,int) but T[] would probably be even more elegant, void* & length also definitely. The issue with T[] is that the efficient comparison effectively only works for primitives and structs that don't redefine Equals. If you have a reference type or a value type that redefines Equals, it'd have to call a virtual method for each element, rendering the efficiency of memcmp useless (a for loop would have been fine here for clarity).

Alternatively one could just redefine the contract to do "binary comparison" only, where for reference types it only compares its references, for struct types it only compares the entire structure regardless whether Equals is redefined. However I don't think it'd be intuitive or useful to users, so limiting it to primitives might still be useful.

Might be a good idea for Span?

Yes, Span would greatly benefit from it aswell, had it in my mind aswell. Just added it for a start.

jamesqo commented 7 years ago

Only skimmed the proposal, but I think System.Runtime.CompilerServices.Unsafe may be a better choice for proposing new APIs than Buffer as the former is not tied to the .NET Framework. There have been lots of new Unsafe APIs recently (e.g. https://github.com/dotnet/corefx/issues/16479) so you may have more success getting it in there.

jkotas commented 7 years ago

All raw memory APIs should go to Span going forward. There is static bool SequenceEqual(this ReadOnlySpan<byte> first, ReadOnlySpan<byte> second) already. I can imagine we can have static int SequenceCompare(this ReadOnlySpan<byte> first, ReadOnlySpan<byte> second) too.

AronParker commented 7 years ago

All raw memory APIs should go to Span going forward. There is static bool SequenceEqual(this ReadOnlySpan first, ReadOnlySpan second) already. I can imagine we can have static int SequenceCompare(this ReadOnlySpan first, ReadOnlySpan second) too.

Fair enough, however I firmly believe that it is important to have comparison functions even outside of Span's, as there are many variants of memcpy but not a single one of memcmp. Nearly all modern programming languages offer a way to compare primitive arrays in a fast, efficient way without too much overhead.

While Span is amazing and it should be there aswell, I do believe there should be a way to access that function without the overhead of a Span. While Span is a great API for working with memory efficiently, I don't think something as specific as Span should be forced onto the user for something general like comparing memory.

jkotas commented 7 years ago

without the overhead of a Span

The overhead of Span is neglible, and it is often less than the overhead of alternatives (passing indices manually, etc.). You can watch a case study on this topic in https://github.com/dotnet/corefxlab/pull/1278 .

AronParker commented 7 years ago

The overhead of Span is neglible, and it is often less than the overhead of alternatives (passing indices manually, etc.). You can watch a case study on this topic in dotnet/corefxlab#1278 .

Alright, I didn't know that. Thanks for the quick reply! I've adjusted the API.

ahsonkhan commented 7 years ago

I don't think something as specific as Span should be forced onto the user for something general like comparing memory.

@AronParker, does this thinking still hold?

Also, can you explain what SequenceCompare should return and in what cases? Essentially, can you provide code sample showing usage of this API?

AronParker commented 7 years ago

@ahsonkhan No. I was actually mainly looking for a way to compare equality, not relation so SequenceEqual is actually sufficient.

Bobris commented 6 years ago

It is pity that this didn't fit Span/ReadOnlySpan. memcmp is critical function for embedded databases.

jkotas commented 6 years ago

@Bobris Span APIs are still work in progress. It was mentioned above that static int SequenceCompare(this ReadOnlySpan<byte> first, ReadOnlySpan<byte> second) may be a reasonable thing to add. We just did not see a good motivation to add it so far - SequenceEqual was sufficient for @AronParker who opened this issue originally.

If you believe that SequenceCompare is an important API to have in additional to SequenceEqual, could you please provide some real examples what you would use it for, and how you would like to see it behave e.g. for spans of different lengths?

Bobris commented 6 years ago

@jkotas Thanks for looking at this. I need lexicographical order in my KeyValue DB. My current pour man implementation is https://github.com/Bobris/BTDB/blob/58c1bf9bbf555e98d836c6de094f431b5dfdd8f9/BTDB/Buffer/BitArrayManipulation.cs#L171. This operation is basic for any B-Tree implementation when key is just sequence of bytes. You probably already saw this https://ayende.com/blog/169825/excerpts-from-the-ravendb-performance-team-report-optimizing-memory-compare-copy-costs ... Would like to not need to use unsafe code.

KrzysztofCwalina commented 6 years ago

I have seen several people asking for this API. I think it would be good to discuss this at the API review. @karelz, I will change the tag to ready for API review. It seems like the API proposal is now for Span APIs, which is what you asked for when you changed the tag to "needs more work".

terrajobst commented 6 years ago

Video

We should change the name to SequenceCompareTo (notice the To at the end).

class static SpanExtensions
{
    static int SequenceCompareTo(this ReadOnlySpan<byte> first, ReadOnlySpan<byte> second);
}
karelz commented 6 years ago

FYI: The API review discussion was recorded - see https://youtu.be/BI3iXFT8H7E?t=4684 (5 min duration)

karelz commented 6 years ago

Top post updated per API name change in API review: https://github.com/dotnet/corefx/issues/16878#issuecomment-349414287

Bobris commented 6 years ago

I looked at implementation mentioned in video in https://github.com/dotnet/corefxlab/blob/master/src/System.Buffers.Primitives/System/Buffers/BufferExtensions.cs and it is correct, but "slow", so hopefully it is planned to have it with optimizations from beginning, please pretty please.

karelz commented 6 years ago

@Bobris what else would you propose? Do you want to submit a PR?

Bobris commented 6 years ago

Not whole PR, but even "hand inlining" CompareTo to simple subtract (lengths are only positive so it is safe from wrapping) is improvement (inputs are identical):

BenchmarkDotNet=v0.10.11, OS=Windows 10 Redstone 2 [1703, Creators Update] (10.0.15063.726) Processor=Intel Core i7-6500U CPU 2.50GHz (Skylake), ProcessorCount=4 Frequency=2531250 Hz, Resolution=395.0617 ns, Timer=TSC .NET Core SDK=2.1.2 [Host] : .NET Core 2.0.3 (Framework 4.6.25815.02), 64bit RyuJIT DefaultJob : .NET Core 2.0.3 (Framework 4.6.25815.02), 64bit RyuJIT

Method Length Mean Error StdDev
ByteByByte 1 2.593 ns 0.0764 ns 0.0715 ns
ByteByByteInline 1 1.727 ns 0.0564 ns 0.0528 ns
ByteByByte 16 21.093 ns 0.4430 ns 0.3699 ns
ByteByByteInline 16 19.231 ns 0.3909 ns 0.3656 ns
ByteByByte 200 298.712 ns 5.9729 ns 9.1213 ns
ByteByByteInline 200 293.655 ns 6.0369 ns 6.9521 ns
ahsonkhan commented 6 years ago

Should this method be a generic method just like SequenceEqual? i.e.

public static int SequenceCompareTo<T>(this ReadOnlySpan<T> first, ReadOnlySpan<T> second) where T : IEquatable<T> { throw null; }
jkotas commented 6 years ago

It would need to be where T : IComparable<T>.