简体   繁体   English

为什么零长度堆栈分配使 C# 编译器乐于允许条件堆栈分配?

[英]Why does a zero-length stackalloc make the C# compiler happy to allow conditional stackallocs?

The following "fix" is very confusing to me;下面的“修复”让我很困惑; the scenario here is conditionally deciding whether to use the stack vs a leased buffer depending on the size - a pretty niche but sometimes-necessary optimization, however: with the "obvious" implementation (number 3, deferring definite assignment until we actually want to assign it), the compiler complains with CS8353:这里的场景是根据大小有条件地决定是使用堆栈还是租用缓冲区 - 这是一个非常利基但有时是必要的优化,但是:使用“明显”的实现(第 3 号,推迟明确的分配,直到我们真正想要分配它),编译器向 CS8353 抱怨:

A result of a stackalloc expression of type 'Span<int>' cannot be used in this context because it may be exposed outside of the containing method 'Span<int>' 类型的 stackalloc 表达式的结果不能在此上下文中使用,因为它可能暴露在包含方法之外

The short repro (a complete repro follows) is:简短的复制(完整的复制如下)是:

// take your pick of:
// Span<int> s = stackalloc[0]; // works
// Span<int> s = default; // fails
// Span<int> s; // fails

if (condition)
{   // CS8353 happens here
    s = stackalloc int[size];
}
else
{
    s = // some other expression
}
// use s here

The only thing I can think here is that the compiler is really flagging that the stackalloc is escaping the context in which the stackalloc happens, and is waving a flag to say "I can't prove whether this is going to be safe later in the method", but by having the stackalloc[0] at the start, we're pushing the "dangerous" context scope higher, and now the compiler is happy that it never escapes the "dangerous" scope (ie it never actually leaves the method, since we're declaring at the top scope).我在这里唯一能想到的是,编译器确实在标记stackallocstackalloc发生 stackalloc 的上下文,并且挥舞着一个标志说“我无法证明这是否会在以后安全方法”,但是通过在开始时使用stackalloc[0] ,我们将“危险”上下文 scope 推得更高,现在编译器很高兴它永远不会逃脱“危险” scope(即它实际上从未离开方法,因为我们在顶级范围内声明)。 Is this understanding correct, and it is just a compiler limitation in terms of what can be proven?这种理解是否正确,就可以证明的内容而言,这只是编译器的限制?

What's really interesting (to me) is that the = stackalloc[0] is fundamentally a no-op anyway , meaning that at least in the compiled form the working number 1 = stackalloc[0] is identical to the failing number 2 = default . (对我来说)真正有趣的是= stackalloc[0]基本上是无操作,这意味着至少在编译形式中,工作编号 1 = stackalloc[0]与失败的编号 2 = default相同。

Full repro (also available on SharpLab to look at the IL ).完整再现(也可在 SharpLab 上查看 IL )。

using System;
using System.Buffers;

public static class C
{
    public static void StackAllocFun(int count)
    {
        // #1 this is legal, just initializes s as a default span
        Span<int> s = stackalloc int[0];
        
        // #2 this is illegal: error CS8353: A result of a stackalloc expression
        // of type 'Span<int>' cannot be used in this context because it may
        // be exposed outside of the containing method
        // Span<int> s = default;
        
        // #3 as is this (also illegal, identical error)
        // Span<int> s;
        
        int[] oversized = null;
        try
        {
            if (count < 32)
            {   // CS8353 happens at this stackalloc
                s = stackalloc int[count];
            }
            else
            {
                oversized = ArrayPool<int>.Shared.Rent(count);
                s = new Span<int>(oversized, 0, count);
            }
            Populate(s);
            DoSomethingWith(s);
        }
        finally
        {
            if (oversized is not null)
            {
                ArrayPool<int>.Shared.Return(oversized);
            }
        }
    }

    private static void Populate(Span<int> s)
        => throw new NotImplementedException(); // whatever
    private static void DoSomethingWith(ReadOnlySpan<int> s)
        => throw new NotImplementedException(); // whatever
    
    // note: ShowNoOpX and ShowNoOpY compile identically just:
    // ldloca.s 0, initobj Span<int>, ldloc.0
    static void ShowNoOpX()
    {
        Span<int> s = stackalloc int[0];
        DoSomethingWith(s);
    }
    static void ShowNoOpY()
    {
        Span<int> s = default;
        DoSomethingWith(s);
    }
}

The Span<T> / ref feature is essentially a series of rules about to which scope a given value can escape by value or by reference. Span<T> / ref功能本质上是一系列规则,scope 给定值可以通过值或引用转义。 While this is written in terms of method scopes it's helpful to simplify to just one of two statements:虽然这是根据方法范围编写的,但将其简化为以下两个语句之一会很有帮助:

  1. The value cannot be return from the method该值不能从方法返回
  2. The value can be returned from the method该值可以从方法返回

The span safety doc goes into great detail about how the scope is calculated for various statements and expressions. 跨度安全文档详细介绍了如何针对各种语句和表达式计算 scope。 The relevant part here though is for how locals are processed.不过,这里的相关部分是关于如何处理本地人的。

The main take away is that whether or not a local can return is calculated at the local declaration time.主要的收获是,本地是否可以返回是在本地声明时计算的。 At the point the local is declared the compiler examines the initializer and makes a decision about whether the local can or cannot be return from the method.在声明局部变量时,编译器检查初始化器并决定局部变量是否可以从方法返回。 In the case there is an initializer then the local will be able to return if the initialization expression is able to be returned.如果有初始化程序,那么如果能够返回初始化表达式,则本地将能够返回。

How do you handle the case where a local is declared but there is no initializer?您如何处理声明了本地但没有初始化程序的情况? The compiler has to make a decision: can it or can it not return?编译器必须做出决定:它可以返回还是不能返回? When designing the feature we made the decision that the default would be "it can be returned" because it's the decision that caused the least amount of friction for existing patterns.在设计该功能时,我们决定默认为“它可以返回”,因为这是对现有模式造成最小摩擦的决定。

That did leave us with the problem of how developers could declare a local that wasn't safe to return but also lacked an initializer.这确实给我们留下了一个问题,即开发人员如何声明一个不能安全返回但又缺少初始化程序的本地。 Eventually we settled on the pattern of = stackalloc [0] .最终我们确定了= stackalloc [0]的模式。 This is an expression that is safe to optimize away and a strong indicator, basically a requirement, that the local isn't safe to return.这是一个可以安全优化的表达式,也是一个强有力的指标,基本上是一个要求,即本地返回是不安全的。

Knowing that this explains the behavior you are seeing:知道这解释了您所看到的行为:

  • Span<int> s = stackalloc[0] : this is not safe to return hence the later stackalloc succeeds Span<int> s = stackalloc[0] :返回是不安全的,因此后面的stackalloc成功
  • Span<int> s = default : this is safe to return because default is safe to return. Span<int> s = default :这是可以安全返回的,因为default可以安全返回。 This means the later stackalloc fails because you're assigning a value that isn't safe to return to a local that is marked as safe to return这意味着后面的stackalloc失败,因为您正在分配一个不安全的值以返回到标记为可安全返回的本地
  • Span<int> s; : this is safe to return because that is the default for unininitialized locals. :这是安全的返回,因为这是未初始化的本地人的默认值。 This means the later stackalloc fails because you're assigning a value that isn't safe to return to a local that is marked as safe to return这意味着后面的stackalloc失败,因为您正在分配一个不安全的值以返回到标记为可安全返回的本地

The real downside to the = stackalloc[0] approach is that it's only applicable to Span<T> . = stackalloc[0]方法的真正缺点是它仅适用于Span<T> It's not a general solution for ref struct .这不是ref struct的通用解决方案。 In practice though it's not as much of a problem for other types.在实践中,尽管对于其他类型来说这不是什么大问题。 There is some speculation on how we could make it more general but not enough evidence to justify doing it at this point.有一些关于我们如何使它更普遍的猜测,但没有足够的证据证明在这一点上这样做是合理的。

Isn't an answer to "why";不是“为什么”的答案; however you could change it to a ternary operator slicing the result of the array assignment to a Span:但是,您可以将其更改为三元运算符,将数组分配的结果切片到 Span:

public static void StackAllocFun(int count)
{
    int[] oversized = null;
    try
    {
        Span<int> s = ((uint)count < 32) ?
            stackalloc int[count] :
            (oversized = ArrayPool<int>.Shared.Rent(count)).AsSpan(0, count);

        Populate(s);
        DoSomethingWith(s);
    }
    finally
    {
        if (oversized is not null)
        {
            ArrayPool<int>.Shared.Return(oversized);
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM