为什么零长度的stackalloc会让c编译器乐于允许条件stackalloc?

ctehm74n  于 2021-07-13  发布在  Java
关注(0)|答案(2)|浏览(253)

下面的“修复”对我来说非常混乱;这里的场景是根据大小有条件地决定是否使用堆栈和租用的缓冲区-这是一个相当小的范围,但有时是必要的优化,然而:对于“明显的”实现(第3个,将确定的赋值推迟到我们真正想要赋值),编译器向cs8353抱怨:
“span”类型的stackalloc表达式的结果不能在此上下文中使用,因为它可能在包含方法外部公开
短复制(完整复制如下)是:

// 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

这里我唯一能想到的是编译器确实在标记 stackalloc 正在逃离 stackalloc 发生了,并且挥舞着旗子说“我不能证明这在以后的方法中是否是安全的”,但是 stackalloc[0] 一开始,我们将“危险的”上下文范围推得更高,现在编译器很高兴它永远不会逃逸“危险的”范围(也就是说,它实际上永远不会离开方法,因为我们在顶部范围声明)。这种理解是正确的吗?这仅仅是编译器在证明什么方面的限制?
(对我来说)真正有趣的是 = stackalloc[0] 基本上是一个no-op,意思是至少在编译的形式中工作数字1 = stackalloc[0] 与失败的数字2相同 = default .
完全复制(也可在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);
    }
}
uelo1irk

uelo1irk1#

这个 Span<T> / ref 特征本质上是一系列规则,关于给定值可以通过值或引用转义到哪个范围。虽然这是根据方法作用域编写的,但简化为两个语句中的一个是有帮助的:
无法从方法返回值
可以从方法返回值
span safety doc非常详细地介绍了如何计算各种语句和表达式的作用域。不过,这里的相关部分是关于如何处理本地人的。
主要的收获是在本地声明时计算本地是否可以返回。在声明local时,编译器检查初始值设定项并决定是否可以从方法返回local。如果存在一个初始值设定项,那么如果能够返回初始化表达式,则本地值将能够返回。
如何处理声明了local但没有初始值设定项的情况?编译器必须做出一个决定:它能不能返回?在设计特性时,我们决定默认值为“it can return”,因为这个决定对现有模式造成的摩擦最小。
这确实给我们留下了一个问题,即开发人员如何声明一个不能安全返回但又缺少初始值设定项的local。最终我们决定了 = stackalloc [0] . 这是一个可以安全地进行优化的表达式,也是一个很强的指标,基本上是一个要求,即本地不安全地返回。
知道这可以解释你所看到的行为: Span<int> s = stackalloc[0] :这是不安全的返回,因此稍后 stackalloc 成功 Span<int> s = default :返回是安全的,因为 default 安全返回。这意味着后者 stackalloc 失败,因为您正在将不安全的值指定给标记为可安全返回的本地值 Span<int> s; :这是安全的,因为这是未初始化的局部变量的默认值。这意味着后者 stackalloc 失败,因为您正在将不安全的值指定给标记为可安全返回的本地值
真正的不利因素 = stackalloc[0] 方法是它只适用于 Span<T> . 这不是一个普遍的解决办法 ref struct . 实际上,对于其他类型的人来说,这并不是什么大问题。有人猜测,我们如何才能使它更普遍,但没有足够的证据来证明在这一点上这样做。

snvhrwxg

snvhrwxg2#

不是“为什么”的答案;但是,您可以将其更改为三元运算符,将数组赋值的结果切片为一个范围:

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);
        }
    }
}

相关问题