Skip to content

Commit

Permalink
RuntimeHelpers.CreateSpan optimization for stackalloc (#57123)
Browse files Browse the repository at this point in the history
  • Loading branch information
svick committed Oct 14, 2021
1 parent 5593bc8 commit 4f9e976
Show file tree
Hide file tree
Showing 14 changed files with 405 additions and 90 deletions.
6 changes: 3 additions & 3 deletions src/Compilers/CSharp/Portable/CodeGen/EmitArrayInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private void EmitArrayInitializers(ArrayTypeSymbol arrayType, BoundArrayInitiali
}
else
{
ImmutableArray<byte> data = this.GetRawData(initExprs);
ImmutableArray<byte> data = GetRawData(initExprs);
_builder.EmitArrayBlockInitializer(data, inits.Syntax, _diagnostics);

if (initializationStyle == ArrayInitializerStyle.Mixed)
Expand Down Expand Up @@ -325,7 +325,7 @@ private void InitializerCountRecursive(ImmutableArray<BoundExpression> inits, re
/// Produces a serialized blob of all constant initializers.
/// Non-constant initializers are matched with a zero of corresponding size.
/// </summary>
private ImmutableArray<byte> GetRawData(ImmutableArray<BoundExpression> initializers)
private static ImmutableArray<byte> GetRawData(ImmutableArray<BoundExpression> initializers)
{
// the initial size is a guess.
// there is no point to be precise here as MemoryStream always has N + 1 storage
Expand All @@ -337,7 +337,7 @@ private ImmutableArray<byte> GetRawData(ImmutableArray<BoundExpression> initiali
return writer.ToImmutableArray();
}

private void SerializeArrayRecursive(BlobBuilder bw, ImmutableArray<BoundExpression> inits)
private static void SerializeArrayRecursive(BlobBuilder bw, ImmutableArray<BoundExpression> inits)
{
if (inits.Length != 0)
{
Expand Down
23 changes: 9 additions & 14 deletions src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1929,24 +1929,19 @@ private void EmitArrayCreationExpression(BoundArrayCreation expression, bool use

private void EmitConvertedStackAllocExpression(BoundConvertedStackAllocExpression expression, bool used)
{
EmitExpression(expression.Count, used);

// the only sideeffect of a localloc is a nondeterministic and generally fatal StackOverflow.
// we can ignore that if the actual result is unused
var initializer = expression.InitializerOpt;
if (used)
{
_sawStackalloc = true;
_builder.EmitOpCode(ILOpCode.Localloc);
EmitStackAlloc(expression.Type, initializer, expression.Count);
}

var initializer = expression.InitializerOpt;
if (initializer != null)
else
{
if (used)
{
EmitStackAllocInitializers(expression.Type, initializer);
}
else
// the only sideeffect of a localloc is a nondeterministic and generally fatal StackOverflow.
// we can ignore that if the actual result is unused

EmitExpression(expression.Count, used: false);

if (initializer is not null)
{
// If not used, just emit initializer elements to preserve possible sideeffects
foreach (var init in initializer.Initializers)
Expand Down
111 changes: 89 additions & 22 deletions src/Compilers/CSharp/Portable/CodeGen/EmitStackAllocInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,133 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.PooledObjects;

namespace Microsoft.CodeAnalysis.CSharp.CodeGen
{
internal partial class CodeGenerator
{
private void EmitStackAllocInitializers(TypeSymbol type, BoundArrayInitialization inits)
private void EmitStackAlloc(TypeSymbol type, BoundArrayInitialization? inits, BoundExpression count)
{
if (inits is null)
{
emitLocalloc();
return;
}

Debug.Assert(type is PointerTypeSymbol || type is NamedTypeSymbol);

var elementType = (type.TypeKind == TypeKind.Pointer
? ((PointerTypeSymbol)type).PointedAtTypeWithAnnotations
: ((NamedTypeSymbol)type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[0]).Type;

bool isReadOnlySpan = TypeSymbol.Equals(
(type as NamedTypeSymbol)?.OriginalDefinition, _module.Compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.ConsiderEverything);

var initExprs = inits.Initializers;

var initializationStyle = ShouldEmitBlockInitializerForStackAlloc(elementType, initExprs);
if (initializationStyle == ArrayInitializerStyle.Element)
bool supportsPrivateImplClass = _module.SupportsPrivateImplClass;
var initializationStyle = ShouldEmitBlockInitializerForStackAlloc(elementType, initExprs, supportsPrivateImplClass);

if (isReadOnlySpan)
{
// ROS<T> is only used here if it has already been decided to use CreateSpan
Debug.Assert(UseCreateSpanForReadOnlySpanInitialization(
_module.GetCreateSpanHelper(elementType.GetPublicSymbol()) is not null, false, elementType, inits, supportsPrivateImplClass));

EmitExpression(count, used: false);

ImmutableArray<byte> data = GetRawData(initExprs);
_builder.EmitCreateSpan(data, elementType.GetPublicSymbol(), inits.Syntax, _diagnostics);
}
else if (initializationStyle == ArrayInitializerStyle.Element)
{
emitLocalloc();
EmitElementStackAllocInitializers(elementType, initExprs, includeConstants: true);
}
else
{
ImmutableArray<byte> data = this.GetRawData(initExprs);
bool mixedInitialized = false;

emitLocalloc();

ImmutableArray<byte> data = GetRawData(initExprs);
if (data.All(datum => datum == data[0]))
{
_builder.EmitStackAllocBlockInitializer(data, inits.Syntax, emitInitBlock: true, _diagnostics);

if (initializationStyle == ArrayInitializerStyle.Mixed)
{
EmitElementStackAllocInitializers(elementType, initExprs, includeConstants: false);
}
_builder.EmitStackAllocBlockSingleByteInitializer(data, inits.Syntax, emitInitBlock: true, _diagnostics);
}
else if (elementType.SpecialType.SizeInBytes() == 1)
{
_builder.EmitStackAllocBlockInitializer(data, inits.Syntax, emitInitBlock: false, _diagnostics);

if (initializationStyle == ArrayInitializerStyle.Mixed)
_builder.EmitStackAllocBlockSingleByteInitializer(data, inits.Syntax, emitInitBlock: false, _diagnostics);
}
else
{
if (_module.GetCreateSpanHelper(elementType.GetPublicSymbol()) is null)
{
EmitElementStackAllocInitializers(elementType, initExprs, includeConstants: false);
EmitElementStackAllocInitializers(elementType, initExprs, includeConstants: true);
mixedInitialized = true;
}
else
{
EmitStackAllocBlockMultiByteInitializer(data, elementType, inits.Syntax, _diagnostics);
}
}
else

if (initializationStyle == ArrayInitializerStyle.Mixed && !mixedInitialized)
{
EmitElementStackAllocInitializers(elementType, initExprs, includeConstants: true);
EmitElementStackAllocInitializers(elementType, initExprs, includeConstants: false);
}
}

void emitLocalloc()
{
EmitExpression(count, used: true);

_sawStackalloc = true;
_builder.EmitOpCode(ILOpCode.Localloc);
}
}

private ArrayInitializerStyle ShouldEmitBlockInitializerForStackAlloc(TypeSymbol elementType, ImmutableArray<BoundExpression> inits)
private void EmitStackAllocBlockMultiByteInitializer(ImmutableArray<byte> data, TypeSymbol elementType, SyntaxNode syntaxNode, DiagnosticBag diagnostics)
{
// get helpers
var definition = (MethodSymbol)_module.Compilation.GetWellKnownTypeMember(WellKnownMember.System_ReadOnlySpan_T__GetPinnableReference)!;
var getPinnableReference = definition.AsMember(definition.ContainingType.Construct(elementType));
var readOnlySpan = ((NamedTypeSymbol)_module.Compilation.CommonGetWellKnownType(WellKnownType.System_ReadOnlySpan_T)).Construct(elementType);

// emit call to the helper
_builder.EmitOpCode(ILOpCode.Dup);
_builder.EmitCreateSpan(data, elementType.GetPublicSymbol(), syntaxNode, diagnostics);

var temp = AllocateTemp(readOnlySpan, syntaxNode);
_builder.EmitLocalStore(temp);
_builder.EmitLocalAddress(temp);
FreeTemp(temp);

// PROTOTYPE: is this safe without pinning?
_builder.EmitOpCode(ILOpCode.Call, 0);
EmitSymbolToken(getPinnableReference, syntaxNode, optArgList: null);
_builder.EmitIntConstant(data.Length);
// PROTOTYPE: is this correct without unaligned.?
_builder.EmitOpCode(ILOpCode.Cpblk, -3);
}

internal static bool UseCreateSpanForReadOnlySpanInitialization(
bool hasCreateSpanHelper, bool considerInitblk, TypeSymbol elementType, BoundArrayInitialization? inits, bool supportsPrivateImplClass) =>
hasCreateSpanHelper && inits?.Initializers is { } initExprs &&
elementType.SpecialType.SizeInBytes() > 1 &&
ShouldEmitBlockInitializerForStackAlloc(elementType, initExprs, supportsPrivateImplClass) == ArrayInitializerStyle.Block &&
// if all bytes are the same, use initblk if able, instead of CreateSpan
(!considerInitblk || (GetRawData(initExprs) is var data && !data.All(datum => datum == data[0])));

private static ArrayInitializerStyle ShouldEmitBlockInitializerForStackAlloc(TypeSymbol elementType, ImmutableArray<BoundExpression> inits, bool supportsPrivateImplClass)
{
if (!_module.SupportsPrivateImplClass)
if (!supportsPrivateImplClass)
{
return ArrayInitializerStyle.Element;
}
Expand Down Expand Up @@ -93,7 +160,7 @@ private ArrayInitializerStyle ShouldEmitBlockInitializerForStackAlloc(TypeSymbol
return ArrayInitializerStyle.Element;
}

private void StackAllocInitializerCount(ImmutableArray<BoundExpression> inits, ref int initCount, ref int constInits)
private static void StackAllocInitializerCount(ImmutableArray<BoundExpression> inits, ref int initCount, ref int constInits)
{
if (inits.Length == 0)
{
Expand Down
11 changes: 11 additions & 0 deletions src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,17 @@ public sealed override Cci.IMethodReference GetInitArrayHelper()
return ((MethodSymbol)Compilation.GetWellKnownTypeMember(WellKnownMember.System_Runtime_CompilerServices_RuntimeHelpers__InitializeArrayArrayRuntimeFieldHandle))?.GetCciAdapter();
}

public sealed override Cci.IMethodReference GetCreateSpanHelper(ITypeSymbol elementType)
{
if ((elementType as Symbols.PublicModel.TypeSymbol)?.UnderlyingTypeSymbol is not { } csharpElementType)
{
return null;
}

return ((MethodSymbol)Compilation.GetWellKnownTypeMember(WellKnownMember.System_Runtime_CompilerServices_RuntimeHelpers__CreateSpan_T))
?.Construct(csharpElementType).GetCciAdapter();
}

public sealed override bool IsPlatformType(Cci.ITypeReference typeRef, Cci.PlatformType platformType)
{
var namedType = typeRef.GetInternalSymbol() as NamedTypeSymbol;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ public override BoundNode VisitConversion(BoundConversion node)

var rewrittenType = VisitType(node.Type);

// special handling for initializers converted to ROS<T>
if (TypeSymbol.Equals(rewrittenType.OriginalDefinition, _compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.ConsiderEverything))
{
if (node.Operand is BoundConvertedStackAllocExpression or BoundArrayCreation)
{
bool hasCreateSpanHelper = _compilation.GetWellKnownTypeMember(WellKnownMember.System_Runtime_CompilerServices_RuntimeHelpers__CreateSpan_T) is not null;

var (isArrayCreation, elementType, initializerOpt, count) = node.Operand switch
{
BoundConvertedStackAllocExpression stackAlloc => (false, stackAlloc.ElementType, stackAlloc.InitializerOpt, VisitExpression(stackAlloc.Count)),
BoundArrayCreation arrayCreation =>
(true, ((ArrayTypeSymbol)arrayCreation.Type).ElementType, arrayCreation.InitializerOpt, /* need some dummy expression */ _factory.Literal(0)),
_ => throw ExceptionUtilities.Unreachable
};

if (CodeGen.CodeGenerator.UseCreateSpanForReadOnlySpanInitialization(hasCreateSpanHelper, considerInitblk: !isArrayCreation, elementType, initializerOpt,
/* PROTOTYPE: how to find out if this is ENC? */ supportsPrivateImplClass: true))
{
return new BoundConvertedStackAllocExpression(node.Operand.Syntax, elementType, count, initializerOpt, rewrittenType);
}
}
}

bool wasInExpressionLambda = _inExpressionLambda;
_inExpressionLambda = _inExpressionLambda || (node.ConversionKind == ConversionKind.AnonymousFunction && !wasInExpressionLambda && rewrittenType.IsExpressionTree());
var rewrittenOperand = VisitExpression(node.Operand);
Expand Down
Loading

0 comments on commit 4f9e976

Please sign in to comment.