Portable (and thus slower) version of Span<>#13562
Conversation
|
cc @KrzysztofCwalina |
| // As the name suggests, the offset is measured in bytes, not T's. | ||
| // | ||
| private readonly object _object; | ||
| private readonly IntPtr _byteOffset; |
There was a problem hiding this comment.
Nit: Vast majority of code in the repo has fields at the top. Can we do the same here?
There was a problem hiding this comment.
Sorry, not a style I favor and not mandated by the guidelines.
There was a problem hiding this comment.
@weshaggard / @stephentoub, thoughts?
It might be not mandated by the guidelines, but it might be better to be consistent with the rest of the repo (and add a guideline :-))
There was a problem hiding this comment.
I would like them to be at the top. It's how we do it basically everywhere else in the repo, and where we don't it's effectively a style bug we fix. If it's not in the guidelines, it should be.
|
I think it is important to have the indexer, ctors and Slice methods inlined. |
Wasn't sure they'd inline with all the parameter validation code but sure, I can always put the MethodImpl attribute on them. |
|
Inlining should be checked on the older runtimes as the current JIT is different (more capable). |
| /// </summary> | ||
| public static bool IsReferenceFree<T>() | ||
| { | ||
| if (PerTypeLatches<T>.IsReferenceFree) |
There was a problem hiding this comment.
This should be just PerTypeLatches<T>.IsReferenceFree and the initialization should done via static constructor in PerTypeLatches
|
|
||
| /// <summary> | ||
| /// Returns a cached empty array (Array.Empty not available on S.R. 4.0.0.0) | ||
| /// </summary> |
There was a problem hiding this comment.
It seems to return new array every time, not cached as stated in the comment
There was a problem hiding this comment.
Oops - yeah - last minute code thrown in in the migrate to corefx...
| /// <summary> | ||
| /// The number of items in the span. | ||
| /// </summary> | ||
| public int Length { get; } |
There was a problem hiding this comment.
A getter-only property is fine to use here, but I wonder if it'd be better for this particular struct to have an explicit readonly _length field (declared along with the other fields), to make it more clear to readers what the size of the struct is.
| @@ -0,0 +1,35 @@ | |||
| // Licensed to the .NET Foundation under one or more agreements. | |||
There was a problem hiding this comment.
What does the ".Microsoft" in the filename refer to? If it's about a particular platform, we should use a word or words that best describes the target.
There was a problem hiding this comment.
Microsoft .NET runtimes (CLR+CoreClr vs. as opposed to say, Mono.)
There was a problem hiding this comment.
Mono is Microsoft runtime too...
There was a problem hiding this comment.
Hence the difficulty in choosing a good name - with the "store as Pinnable" trick, though, we can mothball this file altogether and have a implementation that works on Mono too. So this is moot.
| if (array == null) | ||
| throw new ArgumentNullException(nameof(array)); | ||
| if (default(T) == null && array.GetType() != typeof(T[])) | ||
| throw new ArrayTypeMismatchException(SR.Format(SR.ArrayTypeMustBeExactMatch, typeof(T))); |
There was a problem hiding this comment.
I believe there were some recent JIT improvements around conditionalized exception throwing like this regarding inlining, but even with that, should we separate out these throws into their own methods?
There was a problem hiding this comment.
JIT improvements around conditionalized exception throwing
Note that this implementation is primarily meant for existing full framework runtimes that do not have these improvements.
should we separate out these throws into their own methods?
Yes
| if ((uint)Length > (uint)destination.Length) | ||
| return false; | ||
|
|
||
| // TODO: This is a tide-over implementation as we plan to add a overlap-safe cpblk-based api to Unsafe. |
There was a problem hiding this comment.
Is there an issue # we can add to the TODO?
There was a problem hiding this comment.
| /// <exception cref="System.ArgumentOutOfRangeException"> | ||
| /// Thrown when the specified <paramref name="length"/> is negative. | ||
| /// </exception> | ||
| public static Span<T> DangerousCreate(object obj, ref T rawPointer, int length) |
There was a problem hiding this comment.
Did we decide for the second parameter to be ref T or IntPtr? I honestly don't remember. If we decided for ref T, do you remember why?
There was a problem hiding this comment.
It's ref, because a ref T is easy to come up with.
There was a problem hiding this comment.
"ref T" is how it ended up in the final ApiReview:
https://github.com/dotnet/apireviews/tree/master/2016/11-04-SpanOfT
| /// Start and index are non-negative, and already pre-validated to be within the valid range of their containing Span. | ||
| /// | ||
| /// If the byte length (Span.Length * sizeof(T)) does an unsigned overflow (i.e. the buffer wraps or is too big to fit within the address space), | ||
| /// the behavior is undefined. |
There was a problem hiding this comment.
Should we add a Debug.Assert for it just to help catch some potential misuse?
There was a problem hiding this comment.
I have asserts for start and index being negative (as those are already validated.)
The unsigned overflow is unvalidated at this point and they come from the application so it's not a condition I can assert on.
There was a problem hiding this comment.
so it's not a condition I can assert on
Are there valid uses cases where it would overflow?
There was a problem hiding this comment.
They're probably not valid, per se, (an app would be asking for a Span that wraps the address space), but usually we reserve asserts to detect conditions that are the framework's fault, not the app's fault.
There was a problem hiding this comment.
So if it's possible for an app to get data to this internal helper that's invalid, shouldn't there be an exception to catch that earlier in the call path? And if there is, then we can assert here.
There was a problem hiding this comment.
The only way an unsigned overflow can get here are the two unsafe constructions (.ctor(void*) and DangerousCreate()).
Adding parameter validation to those power routines would probably not go down well around these parts.
I'd be open to adding the assert to the safe constructors with arrays if I could find an unobstrusive way to code it. But between the 32-64 bit dichotomy and all the restrictions on IntPtr arithmetic, such an assert would end up overwhelming the code and it's a really unlikely case since it would mean managed array construction is unsafe.
There was a problem hiding this comment.
The only way an unsigned overflow can get here are the two unsafe constructions (.ctor(void*) and DangerousCreate()).
Ok
|
|
||
| private static bool IsReferenceFree(Type type) | ||
| { | ||
| if (type.GetTypeInfo().IsPrimitive) // This is hopefully the common case. All types that return true for this are value types w/out embedded references. |
There was a problem hiding this comment.
Should we add a check for IsEnum?
| if (srcGreaterThanDst) | ||
| { | ||
| // Source address greater than or equal to destination address. Can do normal copy. | ||
| for (int i = 0; i < Length; i++) |
There was a problem hiding this comment.
Why don't we do a bulk copy here?
| <AssemblyVersion>4.0.3.0</AssemblyVersion> | ||
| <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
| <CLSCompliant>false</CLSCompliant> | ||
| <NuGetTargetMoniker>.NETStandard,Version=v1.0</NuGetTargetMoniker> |
There was a problem hiding this comment.
We should add:
<DocumentationFile>$(OutputPath)$(AssemblyName).xml</DocumentationFile>so that all of your good docs end up getting validated and generated into a docs file.
| { | ||
| get | ||
| { | ||
| switch (_object) |
There was a problem hiding this comment.
This is super slow. Instead, this should be something like:
public struct Span<T>
{
[StructLayout(LayoutKind.Sequential)]
private sealed class Pinnable
{
public T Data;
}
private readonly Pinnable _pinnable;
private readonly IntPtr _byteOffset;
public ref T DangerousGetPinnableReference
{
get
{
return (_pinnable != null) ? Unsafe.AddByteOffset<T>(ref _pinnable.Data, _byteOffset) : Unsafe.AsRef<T>((void*)_byteOffset);
}
}
}
Also, I would manually inline the line into the indexer so that the JIT has less inlining to do for it.
There was a problem hiding this comment.
This will cause us to take a ref of the address within the array header (i.e. the hidden "length+padding" "field"). You may recall I asked a couple of days ago if that was safe to do.
There was a problem hiding this comment.
I do not recall this question being asked ... It is safe to do on .NET Framework/.NET Core runtimes that this implementation seems to be specific to anyway.
There was a problem hiding this comment.
With this trick, it might become less specific as we can avoid hard-coding the Mono layout. But let's go with this for now.
There was a problem hiding this comment.
Won't this be broken for arrays?
There was a problem hiding this comment.
Yeah I couldn't understand why the @jkotas version should not be possible, see https://github.com/dotnet/corefx/issues/13426#issuecomment-259913759 This also means we do not need extra overloads for Unsafe.
|
/cc @davidfowl |
| if (srcGreaterThanDst) | ||
| { | ||
| // Source address greater than or equal to destination address. Can do normal copy. | ||
| for (int i = 0; i < Length; i++) |
There was a problem hiding this comment.
Cache Length into local for better perf?
| } | ||
|
|
||
| [Fact] | ||
| public static void Overlapping2() |
There was a problem hiding this comment.
Why are you not using [Theory] instead of having Overlapping1, Overlapping2, ...?
There was a problem hiding this comment.
Partly because I wrote all this in a desktop infrastructure where I have only a minimal XUnit emulator.
And partly because I frankly don't care for the [Theory] construct. I like unit tests that I can drag out to a simple standalone app for me to study when they break.
There was a problem hiding this comment.
Theories are much better for exactly this reason though though 😄 . Same logical test with different data sets.
There was a problem hiding this comment.
Also, i don't think not using theories scales past span. Many other APIs that we will be adding (e.g. formatting) are so much more complicated than spans that we won't be able to avoid using theories.
| int[] src = { 1, 2, 3 }; | ||
| int[] dst = { 99, 100, 101 }; | ||
|
|
||
| Span<int> srcSpan = new Span<int>(src); |
There was a problem hiding this comment.
How are we going to ensure that the fast-span and slow-span are tested? i.e. the tests run in the context of coreclr, which has the fast span.
There was a problem hiding this comment.
I believe that's done by building a facade for CoreClr. It's not something that can be right now until the CoreClr implementation is brought up to date with the spec.
There was a problem hiding this comment.
BTW: It would be fine to do the reference assembly and CoreCLR façade plumbing right away. Most of the APIs are in CoreCLR already, and it is easy to add baseline for the few APIs that are not there yet.
| /// <exception cref="System.ArgumentOutOfRangeException"> | ||
| /// Thrown when the specified <paramref name="length"/> is negative. | ||
| /// </exception> | ||
| public static Span<T> DangerousCreate(object obj, ref T rawPointer, int length) |
There was a problem hiding this comment.
It's ref, because a ref T is easy to come up with.
| // Disallow "special" objects that have their own object headers and would yield unexpected (and runtime-dependent) results. | ||
| // We already have constructors for arrays (and that enforce the passing of the properly typed single-dimensional array.) | ||
| // This constructor is not useful for strings (cannot express"ref s[i]"). | ||
| if (obj is Array) |
There was a problem hiding this comment.
We're explicitly blocking these? Why?
There was a problem hiding this comment.
string: an immutable type being wrapped inside a mutable Span... and you can't take a "ref s[0]" so there's no correct way to use it anyway.
array: this boils down to - do we want to allow multidim arrays or not? (what guarantees do we make about the layout of those (row order or column order?) We could allow T[] to go through but that's another check and it duplicates functionality that we have more strongly typed constructors for.
The offered up rationale for these were plain old objects with "fixed array-like stuff in them" so that's what I'm scoping these to.
There was a problem hiding this comment.
We should not be in the business of enforcing what kind of stuff can or cannot be used with the DangerousCreate. This API is for very advanced users that know what they are doing:
- It costs perf. The advanced users call this API for perf. Wasting perf for unnecessary checks makes this API useless.
- The advanced users always find a valid reasons for why they want to do something, and these unnecessary checks always get in their way.
There was a problem hiding this comment.
Yes, we definitely don't want all these checks in. And calling ref T rawPointer seems wrong to me, it is not a pointer, it is a ref. The description of this parameter makes this clear, "A reference to data within that object", so call it objectData the ref is clear from the type. It is not a raw pointer at all.
| /// <exception cref="System.IndexOutOfRangeException"> | ||
| /// Thrown when index less than 0 or index greater than or equal to Length | ||
| /// </exception> | ||
| public ref T this[int index] |
There was a problem hiding this comment.
This is correct but none of the tooling outside of corefx supports ref returns. I think we need to return a T for now with a setter and a TODO to unblock this once the tooling is far along enough. I believe these methods would be filtered out if using C#6
There was a problem hiding this comment.
It's not so easy to change after it's out in the wild - it's a breaking change and things that depend on it will have to be recompiled at least. I believe the decision was that we'd accept C#7 being a prereq.
There was a problem hiding this comment.
We shouldn't ship this way (to nuget.org). I'm talking about a few months. Maybe we'll have to expedite that somehow..... Im worried about being able to use this immediately (right after this goes in).
There was a problem hiding this comment.
What prevents you from picking up latest Roslyn beta for your experiments? It is in the current dotnet CLI builds; it is easy to add it to .csproj file if you are using classic VS projects; ... .
We have even said that C# 8 will be eventual prereq for Span to get enforcement that it is used correctly.
There was a problem hiding this comment.
xproj and project.json. Everything else is using that and we'd need to make a new build with C# 7 which nobody is looking at. The next version of Visual Studio will have proper support.... but I'd like to use it with VS2015 and xproj since all of the existing .NET Core tooling is based on that.
There was a problem hiding this comment.
Yes, absolutely, have you tried it? 😄 There is no VS2015 support...
There was a problem hiding this comment.
They use VSCode/Sublime on the CLI itself... I actually like using visual studio
There was a problem hiding this comment.
installing preview3 currently breaks VS 2015 (or exposes a bug in VS depending how you look at it) - and I think VS 2015 will need a patch to work; with Core, when it is installed.
There was a problem hiding this comment.
e.g. you can't File->New to get a working project and need to do all restores from command line https://github.com/dotnet/cli/issues/4248#issuecomment-259551612
There was a problem hiding this comment.
@davidfowl - the C# workaround is in. If it meets with your approval, pls sign off (or better yet, hit the merge button.)
|
@atsushikan You should avoid merging commits during review. This PR is now unreadable as it has 323 changed files... |
|
Unfortunately, these frequent version bumps make it necessary - otherwise, CI's become useless. |
You can rebase instead of merging. |
| <Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> | ||
| <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), dir.props))\dir.props" /> | ||
| <ItemGroup> | ||
| <Project Include="System.Memory.csproj" Condition="'$(OS)'=='Windows_NT'" /> |
There was a problem hiding this comment.
Why is this windows only? (I have no idea how the corefx builds work)
There was a problem hiding this comment.
Because S.R.C.Unsafe is Windows only. No, I don't know why.
There was a problem hiding this comment.
S.R.CS.Unsafe is built on Windows only. I suspect that it is because of ilproj support was not available in Unix buildtools at the time it was introduced. (It is no longer the case so it should be possible to remove this condition even for S.R.CS.Unsafe.)
System.Memory does not need ilasm to build. It uses S.R.CS.Unsafe from package so I do not think there is good reason for having this condition here.
What's the matter, don't we have any version numbers you like??
(all this repeated source is annoying - unfortunately, Rosylin at this point at least, insists that "ref" variables be initialized once only at declaration point and doesn't allow the conditional ternary operator on ref types. This makes it hard to manually inline DangerousGetPinnableReference in a nice way...)
Allow JIT to recognize the throw path as a no-return path so it can throw it into the cold region.
It pessimizes the routine on the desktop CLR.
Description: "Provide classes" => "Provide types" Add fastpath for specific types in IsReferenceFree Make MeasureArrayAdjustment a PerTypeValues private
Eh... hard to tell with all the noise but it did seems to shave a couple of percentage points off.
Ah, the fun of writing Span code w/out stack-only enforcement.
...to typeforward on CoreClr. Right now, CoreClr fails 44 out of 55 tests so I believe it's premature to throw the switch just yet. This just gets some of the grunt work out of the way.
| <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
| <OutputType>Library</OutputType> | ||
| <CLSCompliant>false</CLSCompliant> | ||
| <AssemblyVersion>4.0.0.0</AssemblyVersion> |
After chatting offline, we'll accomodate C# 6.0 for now by having the indexer return "T" rather than "ref T". Some performant code require the "ref T" version so as another stopgap, we'll provide a GetItem() method that returns "ref T". Once the tooling story is in better shape, we'll merge GetItem() and the indexer into a single indexer that returns "ref T".
This is getting really monotonous.
| // TODO: https://github.com/dotnet/corefx/issues/13681 | ||
| // Until we get over the hurdle of C# 7 tooling, this indexer will return "T" and have a setter rather than a "ref T". (The doc comments | ||
| // continue to reflect the original intent of returning "ref T") | ||
| public T this[int index] |
|
I think it's ready to merge after the tests pass. |

Holding off on ReadOnlySpan<> until we're happy
with Span so I don't have keep making fixes in two
places.
Not too happy with the hoops DangerousGetPinnedReference
has to go through: we are looking at an alternative
that involves adding two new Unsafe apis:
https://github.com/dotnet/corefx/issues/13426
I'll have to do some benchmarking to see which one's faster,
though. In the meantime, there's plenty to review
that won't be affected by that.