Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Cache ROS constructed from arrays of constants (remaining types)#69820

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
jcouv merged 15 commits intodotnet:mainfromjcouv:cache-arrays
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
NextNext commit
Cached ROS constructed from arrays of constants (remaining types)
  • Loading branch information
Julien Couvreur committedSep 5, 2023
commiteb6497dc1e7df7b0974265ca514346ebab8467a7
228 changes: 153 additions & 75 deletionssrc/Compilers/CSharp/Portable/CodeGen/EmitArrayInitializer.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@
usingSystem;
usingSystem.Collections.Immutable;
usingSystem.Diagnostics;
usingSystem.Diagnostics.CodeAnalysis;
usingSystem.Linq;
usingSystem.Reflection.Metadata;
usingMicrosoft.CodeAnalysis.CSharp.Symbols;
Expand DownExpand Up@@ -449,6 +450,13 @@ private bool TryEmitReadonlySpanAsBlobWrapper(NamedTypeSymbol spanType, BoundExp
returnfalse;
}

if(inPlaceTargetisnull&&!used)
Copy link
Contributor

@AlekseyTsAlekseyTsSep 11, 2023
edited
Loading

Choose a reason for hiding this comment

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

if (inPlaceTarget is null && !used)

Would it make sense to move this check above the previousif? #Closed

{
// The caller has specified that we're creating a ReadOnlySpan expression that won't be used.
// We needn't emit anything.
returntrue;
}

// The primary optimization here is for byte-sized primitives that can wrap a ReadOnlySpan directly around a pointer
// into a blob. That requires the ReadOnlySpan(void*, int) ctor. If this constructor isn't available, we give up on
// all optimizations. Technically, if this ctor isn't available but the ReadOnlySpan(T[]) constructor is, we could still
Expand All@@ -475,7 +483,7 @@ private bool TryEmitReadonlySpanAsBlobWrapper(NamedTypeSymbol spanType, BoundExp
specialElementType=elementType.EnumUnderlyingTypeOrSelf().SpecialType;
if(!IsTypeAllowedInBlobWrapper(specialElementType))
{
returnfalse;
returnstartisnull&&lengthisnull&&tryEmitAsCachedArrayOfConstants(ac,arrayType,elementType,spanType,used,inPlaceTarget);
}

// Get the data and number of elements that compose the initialization.
Expand DownExpand Up@@ -522,35 +530,9 @@ private bool TryEmitReadonlySpanAsBlobWrapper(NamedTypeSymbol spanType, BoundExp
lengthForConstructor=elementCount;
}

if(inPlaceTargetisnull&&!used)
{
// The caller has specified that we're creating a ReadOnlySpan expression that won't be used.
// We needn't emit anything.
returntrue;
}

if(elementCount==0)
{
// The span is empty. Optimize away the array. This works regardless of the size of the type.
// (We could optimize this even for non-primitives, but it's not currently worthwhile.)

// If this is in-place initialization, call the default ctor.
if(inPlaceTargetis notnull)
{
EmitAddress(inPlaceTarget,Binder.AddressKind.Writeable);
_builder.EmitOpCode(ILOpCode.Initobj);
EmitSymbolToken(spanType,wrappedExpression.Syntax);
if(used)
{
EmitExpression(inPlaceTarget,used:true);
}
}
else
{
// Otherwise, assign it to a default value / empty span.
Debug.Assert(used);
EmitDefaultValue(spanType,used,wrappedExpression.Syntax);
}
emitEmptyReadonlySpan(spanType,wrappedExpression,used,inPlaceTarget);
returntrue;
}

Expand DownExpand Up@@ -607,7 +589,7 @@ private bool TryEmitReadonlySpanAsBlobWrapper(NamedTypeSymbol spanType, BoundExp
// We need to use RuntimeHelpers.CreateSpan / cached array, but the code has requested a subset of the elements.
// That means the code is something like `new ReadOnlySpan<char>(new[] { 'a', 'b', 'c' }, 1, 2)`
// rather than `new ReadOnlySpan<char>(new[] { 'b', 'c' })`. If such a pattern is found to be
// common, this could be augmented toaccomodate it. For now, we just return false to fail
// common, this could be augmented toaccommodate it. For now, we just return false to fail
// to optimize this case.
returnfalse;
}
Expand DownExpand Up@@ -643,55 +625,151 @@ private bool TryEmitReadonlySpanAsBlobWrapper(NamedTypeSymbol spanType, BoundExp
// We're dealing with a multi-byte primitive, and CreateSpan was not available. Get a static field from PrivateImplementationDetails,
// and use it as a lazily-initialized cache for an array for this data:
// new ReadOnlySpan<T>(PrivateImplementationDetails.ArrayField ??= RuntimeHelpers.InitializeArray(new int[Length], PrivateImplementationDetails.DataField));
returnemitAsCachedArrayFromBlob(spanType,wrappedExpression,elementCount,data,refarrayType,elementType);

varrosArrayCtor=(MethodSymbol?)Binder.GetWellKnownTypeMember(_module.Compilation,WellKnownMember.System_ReadOnlySpan_T__ctor_Array,_diagnostics,syntax:wrappedExpression.Syntax,isOptional:true);
if(rosArrayCtorisnull)
// Emit: new ReadOnlySpan<T>(PrivateImplementationDetails.ArrayField ??= RuntimeHelpers.InitializeArray(new int[Length], PrivateImplementationDetails.DataField));
boolemitAsCachedArrayFromBlob(NamedTypeSymbolspanType,BoundExpressionwrappedExpression,intelementCount,ImmutableArray<byte>data,refArrayTypeSymbolarrayType,TypeSymbolelementType)
Copy link
Contributor

@AlekseyTsAlekseyTsSep 11, 2023
edited
Loading

Choose a reason for hiding this comment

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

emitAsCachedArrayFromBlob

It doesn't look like extraction of code into this local function is necessary. The only call site is above its definition. #Closed

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

The main benefit of this extraction is that it makes the codeflow in the main body of the method clearer. The cases become clearer and in particular this last branch is a single case. I've made sure to keep the diff very clean to minimize review overhead.

Copy link
Contributor

@cstoncstonDec 13, 2023
edited
Loading

Choose a reason for hiding this comment

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

emitAsCachedArrayFromBlob

Minor: PerhapstryEmitAsCachedArrayFromBlob, to match withtryEmitAsCachedArrayOfConstants, etc. #Closed

{
// The ReadOnlySpan<T>(T[] array) constructor we need is missing or something went wrong.
returnfalse;
if(!tryGetReadOnlySpanArrayCtor(wrappedExpression.Syntax,outvarrosArrayCtor))
{
returnfalse;
}

// If we're dealing with an array of enums, we need to handle the possibility that the data blob
// is the same for multiple enums all with the same underlying type, or even with the underlying type
// itself. This is addressed by always caching an array for the underlying type, and then relying on
// arrays being covariant between the underlying type and the enum type, so that it's safe to do:
// new ReadOnlySpan<EnumType>(arrayOfUnderlyingType);
// It's important to have a consistent type here, as otherwise the type of the caching field could
// end up changing non-deterministically based on which type for a given blob was encountered first.
// Also, even if we're not dealing with an enum, we still create a new array type that drops any
// annotations that may have initially been associated with the element type; this is similarly to
// ensure deterministic behavior.
arrayType=arrayType.WithElementType(TypeWithAnnotations.Create(elementType.EnumUnderlyingTypeOrSelf()));

varcachingField=_builder.module.GetArrayCachingFieldForData(data,_module.Translate(arrayType),wrappedExpression.Syntax,_diagnostics.DiagnosticBag);
vararrayNotNullLabel=newobject();

// T[]? array = PrivateImplementationDetails.cachingField;
// if (array is not null) goto arrayNotNull;
_builder.EmitOpCode(ILOpCode.Ldsfld);
_builder.EmitToken(cachingField,wrappedExpression.Syntax,_diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitBranch(ILOpCode.Brtrue,arrayNotNullLabel);

// array = new T[elementCount];
// RuntimeHelpers.InitializeArray(token, array);
// PrivateImplementationDetails.cachingField = array;
_builder.EmitOpCode(ILOpCode.Pop);
_builder.EmitIntConstant(elementCount);
_builder.EmitOpCode(ILOpCode.Newarr);
EmitSymbolToken(arrayType.ElementType,wrappedExpression.Syntax);
_builder.EmitArrayBlockInitializer(data,wrappedExpression.Syntax,_diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitOpCode(ILOpCode.Stsfld);
_builder.EmitToken(cachingField,wrappedExpression.Syntax,_diagnostics.DiagnosticBag);

// arrayNotNullLabel:
// new ReadOnlySpan<T>(array)
_builder.MarkLabel(arrayNotNullLabel);
_builder.EmitOpCode(ILOpCode.Newobj,0);
EmitSymbolToken(rosArrayCtor.AsMember(spanType),wrappedExpression.Syntax,optArgList:null);
returntrue;
}

// Emit: new ReadOnlySpan<ElementType>(PrivateImplementationDetails.cachingField ??= new ElementType[] { ... constants ... })
booltryEmitAsCachedArrayOfConstants(BoundArrayCreationarrayCreation,ArrayTypeSymbolarrayType,TypeSymbolelementType,NamedTypeSymbolspanType,boolused,BoundExpression?inPlaceTarget)
{
varinitializer=arrayCreation.InitializerOpt;
if(initializer==null)
{
returnfalse;
Copy link
Contributor

@AlekseyTsAlekseyTsDec 15, 2023
edited by jcouv
Loading

Choose a reason for hiding this comment

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

return false;

Are we going to get here forobject? Is this intentional? #Resolved

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Yes, we get here forobject (seeReadOnlySpanFromArrayOfConstants_Null) and it's intentional

}

varinitializers=initializer.Initializers;
if(initializers.Any(static init=>init.ConstantValueOpt==null))
Copy link
Contributor

@AlekseyTsAlekseyTsSep 11, 2023
edited by jcouv
Loading

Choose a reason for hiding this comment

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

init.ConstantValueOpt == null

I am not sure if caching an array of strings is such a good idea. This will hold on to strings forever, at the same time this could be the only time they are used. #Resolved

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Tagging@stephentoub for thoughts

Copy link
Member

@stephentoubstephentoubSep 11, 2023
edited
Loading

Choose a reason for hiding this comment

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

This will hold on to strings forever, at the same time this could be the only time they are used.

As string literals, won't they already be held on to forever as part of interning?

Even without that, this doesn't seem different to me from, say, lambda/delegate caching, where the first time a static lambda is used we cache a delegate to it, and we'll hold onto that delegate forever even if we never use it again.

Copy link
Contributor

@AlekseyTsAlekseyTsSep 11, 2023
edited
Loading

Choose a reason for hiding this comment

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

As string literals, won't they already be held on to forever as part of interning?

To be honest, I do not know the answer. And whether the answer is the same for all flavors of frameworks out there.

this doesn't seem different to me from, say, lambda/delegate caching

Strings could be quite big though. And there could be a lot of them in a single initialization. Also, we do not cache delegates when they are created by usingnew. So, there is some control over that form of caching.

Copy link
Member

Choose a reason for hiding this comment

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

To be honest, I do not know the answer. And whether the answer is the same for all flavors of frameworks out there.

It should be true for both .NET Framework and .NET Core, every time we encounter a string literal we add it to a global hashmap (string interning Stephen mentioned above) where it's essentially rooted forever. Except the cases with unloadable ALCs but I guess it should not be a problem here as well. E.g.

voidTest(boolcond){if(cond)Console.WriteLine("true!!");elseConsole.WriteLine("false!!");}

When JIT compiles this method (on its first execution) it will create string objects for both literals even if one of them (e.g.false!!!) will never be used - we might make it more efficient in future, but it's a current behavior of .NET Framework and .NET Core.

Copy link
Member

Choose a reason for hiding this comment

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

I would even say that the behavior change caused by extraction of a local, or an inline of a local, might come as a big surprise.

There are plenty of situations where that's the case, including in the brand new collection expressions feature.

Copy link
Contributor

Choose a reason for hiding this comment

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

We would need to revisit every single use of this in dotnet/runtime where the source might be compiled downlevel, as this could regress all of them. Are you planning to do that?

If we decide that the behavior change, which, If remember correctly, was introduced without much discussion at the time (and likely specifically for the benefit of a single component in development at the time) was a mistake, and should be changed, then we will have to decide what to do with the component.

Copy link
Contributor

Choose a reason for hiding this comment

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

There are plenty of situations where that's the case, including in the brand new collection expressions feature.

Doesn't mean it is a good thing. The nature of differences is not the same, and the impact is quite different. Each situation is somewhat unique.

Copy link
Member

Choose a reason for hiding this comment

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

I agree there is a non-zero chance this causes unexpected behavior for a customer. There are definitely customers out there that generate largestring[] for initialization purposes that are effectively single use. I cannot specifically remember a case where it would combined withReadOnlySpan<string> such that it would trigger this optimization but it's certainly possible.

There are other optimizations we've taken in the past that had the potential to negatively impact customer scenarios. Even simple optimizations like increased method group to delegate caching broke partner teams. It is always going to be a trade off.

The criteria I usually consider is:

  1. Is this on the whole going to be an improvement? In this case I believe the answer is yes it's overall going to produce significant wins compared to the potential downside.
  2. Is there a reasonable and documented way the user canundo the optimization if it's found to be negative? Consider as an example for method group caching theundo operation was just make the delegate allocation explicit:= new Action(Method) vs.= Method. What is theundo operation here? I believe assigning to an intermediate local would subvert the optimization. Is that the way we want to document? Whatever the answer is I would like for it to be explicitly listed in the PR / issue for customers to see.
  3. Are we violating anything in the language specification? For method group to delegate allocation we had to go back and confirm with LDM that they were okay with this change.

Assuming we have resolutions for (2) and (3) I would overall be in favor of moving forward. I tihnk we should consider an entry in the breaking change list though.

stephentoub and jcouv reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

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

Was asked if I could make it clearer which optimization path I preferred: weak reference or not.

I would lean towards starting the non-weak reference approach. My rationale is

  1. Customers who find the behavior undesirable can use theundo mechanism
  2. If this does produce enough negative customer feedback we could flip to theWeakReference approach in an update / servicing fix.

stephentoub and jcouv reacted with thumbs up emoji
{
returnfalse;
}

if(!tryGetReadOnlySpanArrayCtor(arrayCreation.Syntax,outvarrosArrayCtor))
{
returnfalse;
Copy link
Contributor

@AlekseyTsAlekseyTsSep 13, 2023
edited
Loading

Choose a reason for hiding this comment

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

return false;

Is this code path covered by tests? #Closed

}

Debug.Assert(!elementType.IsEnumType());

ImmutableArray<ConstantValue>constants=initializers.Select(static init=>init.ConstantValueOpt!).ToImmutableArray();

if(constants.IsEmpty)
Copy link
Contributor

@AlekseyTsAlekseyTsSep 11, 2023
edited
Loading

Choose a reason for hiding this comment

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

if (constants.IsEmpty)

It looks like we don't needrosArrayCtor to handle this case #Closed

{
emitEmptyReadonlySpan(spanType,arrayCreation,used,inPlaceTarget);
Copy link
Contributor

@AlekseyTsAlekseyTsSep 11, 2023
edited
Loading

Choose a reason for hiding this comment

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

emitEmptyReadonlySpan

Does it mean that empty spans are optimized regardless of element type? If so, consider doing this optimization earlier and in one place, rather than having two different places checking for the same condition. #Closed

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

Moving that optimization earlier in the existing code affects some scenarios. Since that is not the purpose of the PR, I kept the original optimization in it's existing location.

Copy link
Contributor

Choose a reason for hiding this comment

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

Moving that optimization earlier in the existing code affects some scenarios.

It doesn't look this way to me. We can discuss offline in more details

returntrue;
}

Cci.IFieldReferencecachingField=_builder.module.GetArrayCachingFieldForConstants(constants,_module.Translate(arrayType),
arrayCreation.Syntax,_diagnostics.DiagnosticBag);

vararrayNotNullLabel=newobject();

// T[]? array = PrivateImplementationDetails.cachingField;
// if (array is not null) goto arrayNotNull;
_builder.EmitOpCode(ILOpCode.Ldsfld);
_builder.EmitToken(cachingField,arrayCreation.Syntax,_diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitBranch(ILOpCode.Brtrue,arrayNotNullLabel);

// array = arrayCreation;
// PrivateImplementationDetails.cachingField = array;
_builder.EmitOpCode(ILOpCode.Pop);
EmitExpression(arrayCreation,used:true);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitOpCode(ILOpCode.Stsfld);
_builder.EmitToken(cachingField,arrayCreation.Syntax,_diagnostics.DiagnosticBag);

// arrayNotNullLabel:
// new ReadOnlySpan<T>(array)
_builder.MarkLabel(arrayNotNullLabel);
_builder.EmitOpCode(ILOpCode.Newobj,0);
EmitSymbolToken(rosArrayCtor.AsMember(spanType),arrayCreation.Syntax,optArgList:null);

returntrue;
}

// The span is empty. Optimize away the array. This works regardless of the size of the type.
voidemitEmptyReadonlySpan(NamedTypeSymbolspanType,BoundExpressionwrappedExpression,boolused,BoundExpression?inPlaceTarget)
{
// If this is in-place initialization, call the default ctor.
if(inPlaceTargetis notnull)
{
EmitAddress(inPlaceTarget,Binder.AddressKind.Writeable);
_builder.EmitOpCode(ILOpCode.Initobj);
EmitSymbolToken(spanType,wrappedExpression.Syntax);
if(used)
{
EmitExpression(inPlaceTarget,used:true);
}
}
else
{
// Otherwise, assign it to a default value / empty span.
Debug.Assert(used);
EmitDefaultValue(spanType,used,wrappedExpression.Syntax);
}
}

booltryGetReadOnlySpanArrayCtor(SyntaxNodesyntax,[NotNullWhen(true)]outMethodSymbol?rosArrayCtor)
{
rosArrayCtor=(MethodSymbol?)Binder.GetWellKnownTypeMember(_module.Compilation,WellKnownMember.System_ReadOnlySpan_T__ctor_Array,_diagnostics,syntax:syntax,isOptional:true);
if(rosArrayCtorisnull)
{
// The ReadOnlySpan<T>(T[] array) constructor we need is missing or something went wrong.
returnfalse;
}

Debug.Assert(!rosArrayCtor.HasUnsupportedMetadata);
returntrue;
}
Debug.Assert(!rosArrayCtor.HasUnsupportedMetadata);

// If we're dealing with an array of enums, we need to handle the possibility that the data blob
// is the same for multiple enums all with the same underlying type, or even with the underlying type
// itself. This is addressed by always caching an array for the underlying type, and then relying on
// arrays being covariant between the underlying type and the enum type, so that it's safe to do:
// new ReadOnlySpan<EnumType>(arrayOfUnderlyingType);
// It's important to have a consistent type here, as otherwise the type of the caching field could
// end up changing non-deterministically based on which type for a given blob was encountered first.
// Also, even if we're not dealing with an enum, we still create a new array type that drops any
// annotations that may have initially been associated with the element type; this is similarly to
// ensure deterministic behavior.
arrayType=arrayType.WithElementType(TypeWithAnnotations.Create(elementType.EnumUnderlyingTypeOrSelf()));

varcachingField=_builder.module.GetArrayCachingFieldForData(data,_module.Translate(arrayType),wrappedExpression.Syntax,_diagnostics.DiagnosticBag);
vararrayNotNullLabel=newobject();

// T[]? array = PrivateImplementationDetails.cachingField;
// if (array is not null) goto arrayNotNull;
_builder.EmitOpCode(ILOpCode.Ldsfld);
_builder.EmitToken(cachingField,wrappedExpression.Syntax,_diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitBranch(ILOpCode.Brtrue,arrayNotNullLabel);

// array = new T[elementCount];
// RuntimeHelpers.InitializeArray(token, array);
// PrivateImplementationDetails.cachingField = array;
_builder.EmitOpCode(ILOpCode.Pop);
_builder.EmitIntConstant(elementCount);
_builder.EmitOpCode(ILOpCode.Newarr);
EmitSymbolToken(arrayType.ElementType,wrappedExpression.Syntax);
_builder.EmitArrayBlockInitializer(data,wrappedExpression.Syntax,_diagnostics.DiagnosticBag);
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitOpCode(ILOpCode.Stsfld);
_builder.EmitToken(cachingField,wrappedExpression.Syntax,_diagnostics.DiagnosticBag);

// arrayNotNullLabel:
// new ReadOnlySpan<T>(array)
_builder.MarkLabel(arrayNotNullLabel);
_builder.EmitOpCode(ILOpCode.Newobj,0);
EmitSymbolToken(rosArrayCtor.AsMember(spanType),wrappedExpression.Syntax,optArgList:null);
returntrue;
}

/// <summary>Gets whether the element type of an array is appropriate for storing in a blob.</summary>
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp