为什么零长度的 stackalloc 使 C# 编译器乐于允许有条件的 stackalloc?
Why does a zero-length stackalloc make the C# compiler happy to allow conditional stackallocs?
下面的“修复”让我很困惑;这里的场景是根据大小有条件地决定是使用堆栈还是租用缓冲区——这是一个非常小众但有时是必要的优化,但是:使用“显而易见”的实现(第 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
简短的复制(后面是完整的复制)是:
// 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]
根本上是一个无操作 无论如何,这意味着至少在编译后的形式 工作编号 1 = stackalloc[0]
与故障编号 2 = default
.
相同
完整重现(也 available on SharpLab to look at the 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);
}
}
不是“为什么”的答案;但是您可以将其更改为三元运算符,将数组赋值的结果切片到 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);
}
}
}
Span<T> / ref
特性本质上是一系列关于给定值可以按值或按引用转义到哪个范围的规则。虽然这是根据方法范围编写的,但它有助于简化为两个语句之一:
- 无法从方法return编辑值
- 值可以从方法return中编辑
处理完span safety doc goes into great detail about how the scope is calculated for various statements and expressions. The relevant part here though is for how locals个。
主要的收获是本地罐头是否 return 是在本地申报时计算的。在声明局部变量时,编译器检查初始化器并决定局部变量是否可以是方法中的 return。在有初始化器的情况下,如果初始化表达式能够被 returned.
,则局部变量将能够 return
如何处理声明了局部变量但没有初始化程序的情况?编译器必须做出决定:可以还是不可以return?在设计该功能时,我们决定默认设置为“可以 returned”,因为该决定对现有模式造成的摩擦最少。
这确实给我们留下了一个问题,即开发人员如何声明一个对 return 不安全但又缺少初始化程序的局部变量。最终我们确定了 = stackalloc [0]
的模式。这是一个可以安全优化的表达式,也是一个强有力的指标,基本上是一个要求,即本地对 return.
不安全
知道这可以解释您所看到的行为:
Span<int> s = stackalloc[0]
:这对 return 不安全,因此后面的 stackalloc
成功
Span<int> s = default
:这对 return 是安全的,因为 default
对 return 是安全的。这意味着后面的 stackalloc
失败,因为您将对 return 不安全的值分配给标记为 return 安全的本地值
Span<int> s;
:这对 return 是安全的,因为这是未初始化的局部变量的默认值。这意味着后面的 stackalloc
失败,因为您将对 return 不安全的值分配给标记为 return 安全的本地值
= stackalloc[0]
方法的真正缺点是它仅适用于 Span<T>
。这不是 ref struct
的通用解决方案。在实践中,尽管对于其他类型来说这并不是什么大问题。关于我们如何做到这一点有一些猜测 more general 但目前没有足够的证据证明这样做是合理的。
下面的“修复”让我很困惑;这里的场景是根据大小有条件地决定是使用堆栈还是租用缓冲区——这是一个非常小众但有时是必要的优化,但是:使用“显而易见”的实现(第 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
简短的复制(后面是完整的复制)是:
// 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]
根本上是一个无操作 无论如何,这意味着至少在编译后的形式 工作编号 1 = stackalloc[0]
与故障编号 2 = default
.
完整重现(也 available on SharpLab to look at the 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);
}
}
不是“为什么”的答案;但是您可以将其更改为三元运算符,将数组赋值的结果切片到 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);
}
}
}
Span<T> / ref
特性本质上是一系列关于给定值可以按值或按引用转义到哪个范围的规则。虽然这是根据方法范围编写的,但它有助于简化为两个语句之一:
- 无法从方法return编辑值
- 值可以从方法return中编辑
处理完span safety doc goes into great detail about how the scope is calculated for various statements and expressions. The relevant part here though is for how locals个。
主要的收获是本地罐头是否 return 是在本地申报时计算的。在声明局部变量时,编译器检查初始化器并决定局部变量是否可以是方法中的 return。在有初始化器的情况下,如果初始化表达式能够被 returned.
,则局部变量将能够 return如何处理声明了局部变量但没有初始化程序的情况?编译器必须做出决定:可以还是不可以return?在设计该功能时,我们决定默认设置为“可以 returned”,因为该决定对现有模式造成的摩擦最少。
这确实给我们留下了一个问题,即开发人员如何声明一个对 return 不安全但又缺少初始化程序的局部变量。最终我们确定了 = stackalloc [0]
的模式。这是一个可以安全优化的表达式,也是一个强有力的指标,基本上是一个要求,即本地对 return.
知道这可以解释您所看到的行为:
Span<int> s = stackalloc[0]
:这对 return 不安全,因此后面的stackalloc
成功Span<int> s = default
:这对 return 是安全的,因为default
对 return 是安全的。这意味着后面的stackalloc
失败,因为您将对 return 不安全的值分配给标记为 return 安全的本地值
Span<int> s;
:这对 return 是安全的,因为这是未初始化的局部变量的默认值。这意味着后面的stackalloc
失败,因为您将对 return 不安全的值分配给标记为 return 安全的本地值
= stackalloc[0]
方法的真正缺点是它仅适用于 Span<T>
。这不是 ref struct
的通用解决方案。在实践中,尽管对于其他类型来说这并不是什么大问题。关于我们如何做到这一点有一些猜测 more general 但目前没有足够的证据证明这样做是合理的。