-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Proposal: ImmutableArray.UnsafeFreeze<T>(T[]) #25461
Comments
We had methods like these, but they were removed: dotnet/corefx#196 The current "solution" is to use hack like what you have done. You can find another version of the hack in dotnet/corefx#196. Multiple versions of this hack are replicating in uncontrolled way. I have even seen one in F#. I think it is a much worse situation than having officially santioned unsafe interop methods. Maybe we should reconsider? |
@jkotas Thanks, I didn't know about dotnet/corefx#196. I quite like the idea of using a I agree with you, the status quo of proliferating unsupported hacks and workarounds is bad for everyone: bad for users who have to write ugly and dangerous code, and bad for you guys who have to consider users who may be depending on private implementation details. |
The leanest and fastest version of this hack is to use System.Runtime.CompilerServices.Unsafe. With that, the hack is one-linker like this:
|
The whole point of There is a number of patterns, such as argument validation without making private copies that rely on instances never changing values. If we could rely on a convention, we would not need the type and just use ordinary arrays. I am a bit amused that we have to defend that Using |
Another solution could be just using ordinary arrays on the hot internal paths. I can see how |
I agree that getting around immutability is fundamentally unsafe operation. The question is whether we should have a API to codify the unsafe pattern instead of telling everybody to depend on internal implementation details. For example, we have introduced |
I looked into why the safe code is so much slower than the unsafe code, since a good optimizing compiler should be able to make the performance of using My conclusion is that adding a new dangerous API is not necessary. What is necessary is:
With these two changes, by my measurements, the performance is only 1.24× slower than If you want more details, here's what I did: First I looked at the disassembly of When I looked at the disassembly of the previous method, I noticed the presence of (inlined) When I looked at the disassembly, I noticed that the indexer was not being inlined. So I tweaked its code by extracting throw into a separate method. This meant that the indexer was now being inlined, resulting in performance of this method, I ran this on .NET Core 2.1.0-preview2-26131-06. The code is here, the raw results were:
|
More powerful builder helps in some cases, but it won't ever solve the interop case when you need to interop efficiently with the existing code that expects regular arrays and guarantees immutability via documentation. Here is an example of this use case from Roslyn: https://github.com/dotnet/roslyn/blob/944ae114140ba77cbd4be370bf13a7f758f740b3/src/Compilers/Core/Portable/NativePdbWriter/PdbWriter.cs#L593 |
Due to lack of recent activity, this issue has been marked as a candidate for backlog cleanup. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will undo this process. This process is part of the experimental issue cleanup initiative we are currently trialing in a limited number of areas. Please share any feedback you might have in the linked issue. |
This issue will now be closed since it had been marked |
I just stumbled upon this as well, and strongly agree with @jkotas here:
Not having an API for this forces library authors to either take an extra copy when they can guarantee the array doesn't have other owners that could mutate it, or just use the To add context, in my case I'm getting some bytecode from native APIs and wanted to return it as an Consider the following code (ignore the details): using ComPtr<IDxcBlob> dxcBlobBytecode = ShaderCompiler.Instance.CompileShader(hlslSource.AsSpan());
byte* buffer = (byte*)dxcBlobBytecode.Get()->GetBufferPointer();
int length = checked((int)dxcBlobBytecode.Get()->GetBufferSize());
byte[] array = new ReadOnlySpan<byte>(buffer, length).ToArray();
// This is fine, as I can guarantee I'm the sole owner of the array.
// Why should we waste a second allocaiton/copy of the whole buffer here?
return Unsafe.As<byte[], ImmutableArray<byte>>(ref array); I reckon this is a case where having a proper |
@Sergio0694 @svick's code seem to be working acceptably fast without any hacks (#25461 (comment)) - it would be better if capacity version also worked equally fast but IMO it's better to spend time on making this optimization work faster than adding new API. byte array is an implementation detail and it should not be exposed as public API. Obscuring API it doesn't change anything here. |
@Sergio0694 I think the approved var span = new ReadOnlySpan<byte>(buffer, length);
return ImmutableArray.Create(span); |
Hey @krwq, thank you for chiming in! @svick's code will not work in my scenario, as I'm working with a native buffer, so the best I can do is getting a To recap: using ComPtr<IDxcBlob> dxcBlobBytecode = ShaderCompiler.Instance.CompileShader(hlslSource.AsSpan());
byte* buffer = (byte*)dxcBlobBytecode.Get()->GetBufferPointer();
int length = checked((int)dxcBlobBytecode.Get()->GetBufferSize());
ImmutableArray<byte>.Builder builder = ImmutableArray.CreateBuilder<byte>(length);
builder.AddRange(new ReadOnlySpan<byte>(buffer, length)); // This API is missing (also, why?)
ImmutableArray<byte> bytecode = builder.MoveToImmutable(); Should we open a separate proposal for that? I don't really understand why a
I have to say, the current approach of considering EDIT: ah, @svick beat me to it ahahah |
Use case: efficiently creating an
ImmutableArray
with a fixed (>4) number of elements.Presently if you need to create an
ImmutableArray<T>
with a known collection of elements, the most efficient way to do so is to create anImmutableArray<T>.Builder
, set itsCapacity
to allocate an array of the correct size, fill theBuilder
with repeated calls toAdd
, and then useMoveToImmutable
to create theImmutableArray
. (This strategy can be marginally improved by storing a cachedBuilder
in aThreadLocal
.)For element counts smaller than 4, there are overloads of
ImmutableArray.Create
which are implemented by creating an array and then immediately turning it into anImmutableArray
, but for more than 4Create
takes aparams[]
and copies it into anImmutableArray
. There is no way to create an array directly, fill it, and freeze it into anImmutableArray
without copying while pinky-promising never to mutate the original array again.I set up a microbenchmark (see below) which uses codegen to call
ImmutableArray<T>
's privateT[]
constructor, and it's around 4-5 times faster for an array of 1000long
s. I found that a library of mine was spending a reasonable chunk of its time creatingImmutableArray
s, and implementing this hack led to a global ~10% speedup in my end-to-end benchmarks. I'm not entirely happy about depending upon private implementation details like that though.I propose adding a officially-supported static method to freeze an array into an
ImmutableArray
without copying it. I suggest calling itUnsafeFreeze
to emphasise the fact that this method is risky: to use it correctly you have to make sure never to mutate the array that you passed in. (This name may be confusing given that it doesn't have anything to do with C#'sunsafe
; I'm open to alternative suggestions.)There may be a use case for a similar
UnsafeThaw
method which turns anImmutableArray<T>
into aT[]
without copying, but I can't think of one right now.NB. There is precedent in the BCL for APIs which ask you to relinquish ownership of the argument.
ArrayPool.Return
requires you not to continue using the array you returned because it is liable to get handed out to a future caller ofRent
.Here's the code for the microbenchmark I mentioned above (using BenchmarkDotNet):
And the results:
The text was updated successfully, but these errors were encountered: