当前位置: 首页 > 知识库问答 >
问题:

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

屈俊远
2023-03-14

下面的“搞定”对我来说很迷惑;这里的场景是根据大小有条件地决定是否使用堆栈还是租用缓冲区--这是一个相当小的小范围,但有时--必要的优化,然而:对于“明显的”实现(第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);
    }
}

共有1个答案

龚伯寅
2023-03-14

span /ref 特性本质上是一系列规则,给定值可以通过值或引用转义到哪个作用域。虽然这是根据方法范围编写的,但简化为两个语句中的一个很有帮助:

  1. 无法从方法返回值
  2. 值可以从方法返回

span安全文档详细介绍了如何为各种语句和表达式计算作用域。这里的相关部分是如何处理本地数据的。

主要带走的是,一个本地能否返回是在本地申报时间计算的。在声明本地时,编译器检查初始值设定项,并决定是否可以从方法返回本地值。如果有一个初始化器,那么如果能够返回初始化表达式,则本地将能够返回。

如何处理声明了局部值但没有初始值设定项的情况?编译器要做一个决定:它能返回还是不能返回?在设计特性时,我们决定默认值为“It can be return”,因为这是对现有模式造成最小摩擦的决定。

这确实给我们留下了一个问题,即开发人员如何声明一个返回时不安全但又缺少初始化器的本地。最终我们确定了=stackalloc[0]的模式。这是一个可以安全优化的表达式,也是一个很强的指标,基本上是一个要求,表明本地返回时不安全。

知道这解释了你所看到的行为:

  • span s=stackalloc[0] :返回该值不安全,因此后面的stackalloc成功
  • span s=default :返回该值是安全的,因为返回default是安全的。这意味着后面的stackalloc会失败,因为您将不安全返回的值分配给标记为安全返回的本地
  • span s; :返回该值是安全的,因为这是未初始化的局部变量的默认值。这意味着后面的stackalloc会失败,因为您将不安全返回的值分配给标记为安全返回的本地

=stackalloc[0]方法的真正缺点是它只适用于span 。它不是ref struct的通用解决方案。但在实践中,这对其他类型的人来说并不是一个大问题。有一些猜测,我们可以如何使它更普遍,但没有足够的证据证明这样做的理由在这一点上。

 类似资料:
  • 这与我对“流减少不兼容类型”的回答有关。我不知道为什么我的建议有效,霍尔格正确地敦促我这一点。但即使是他似乎也没有一个明确的解释为什么它能起作用。所以,让我们把它作为自己的问题来问: 以下代码不能在中编译(下面指向ideone的链接是,请参阅http://ideone.com/faq): 这样做是正确的:或者将这个流中的两个谓词结合在一起,就像写: IdeOne演示 尽管事实上我想尝试这一点,但我

  • 问题内容: 来自C语言的Go语言最值得注意的方面之一是,如果在其中声明了一个未使用的变量,编译器将不会编译您的程序。那么,如果在函数中声明了一个未使用的参数,那么为什么要构建此程序呢? 问题答案: 没有正式的原因,但是在golang-nuts上给出的原因是: 未使用的变量始终是编程错误,而编写不使用其所有参数的函数是很常见的。 可以将这些参数保留为未命名(使用_),但这可能会与诸如 func fo

  • 这是CppCon谈话中的一个例子https://www.youtube.com/watch?v=F6Ipn7gCOsY 目标是首先从A打印Hello,然后允许线程B启动。很明显,应该避免繁忙等待,因为它占用大量CPU。 作者说, 循环可以由编译器进行优化(通过将 的值放入寄存器中),因为编译器看到 从不Hibernate,因此永远不会被更改。但是,即使线程从不Hibernate,另一个线程仍然可以

  • 问题内容: 我想知道为什么我们通过编译显示“ Hello,World!”的.c文件得到.o文件。是否大于Java .class文件,该文件也显示“ Hello,World!”? 问题答案: Java使用字节码来独立于平台并进行“预编译”,但是字节码由解释器使用并且被提供为足够紧凑,因此您在已编译的C程序中看到的机器代码并不相同。只需看一下Java编译的完整过程即可: 这是Java程序到机器代码转换

  • 然而,gcc和clang都没有这样优化它--它们都在优化级别生成以下内容: 我的问题是:是因为代码太特殊而不需要优化,还是因为不是对的引用,所以不需要这样的优化?似乎唯一的原因可能是可能有一个非-或-值,但在读取它时没有未定义的行为,但我不确定这是否可能。

  • 我想知道为什么java编译器允许在方法声明中抛出,而方法永远不会抛出异常。因为“throws”是处理异常的一种方式(告诉调用方处理它)。 因为有两种处理异常的方法(抛出和try/catch)。在try/catch中,它不允许捕获try块中未抛出的异常,但它允许在不抛出异常的方法中抛出。