C#中的Will‘params’总是会在每次调用时分配一个新数组
本文关键字:数组 分配 新数组 一个 调用 params Will 中的 | 更新日期: 2023-09-27 17:59:24
C#/.NET通过引用传递Array
类型来获得可变函数参数(而C/C++只是将所有值直接放在堆栈上,无论好坏)。
在C#世界中,这有一个巧妙的优势,允许您使用"原始"参数或可重复使用的数组实例调用相同的函数:
CultureInfo c = CultureInfo.InvariantCulture;
String formatted0 = String.Format( c, "{0} {1} {2}", 1, 2, 3 );
Int32 third = 3;
String formatted0 = String.Format( c, "{0} {1} {2}", 1, 2, third );
Object[] values = new Object[] { 1, 2, 3 };
String formatted1 = String.Format( c, "{0} {1} {2}", values );
这意味着生成的CIL相当于:
String formatted0 = String.Format( c, "{0} {1} {2}", new Object[] { 1, 2, 3 } );
Int32 third = 3;
String formatted0 = String.Format( c, "{0} {1} {2}", new Object[] { 1, 2, third } );
Object[] values = new Object[] { 1, 2, 3 };
String formatted1 = String.Format( c, "{0} {1} {2}", values );
这意味着(在非优化JIT编译器中)每个调用都将分配一个新的Object[]
实例-尽管在第三个示例中,您可以将数组存储为字段或其他可重用值,以消除每次调用String.Format
时的新分配。
但是在官方的CLR运行时和JIT中,是否进行了任何优化来消除这种分配?或者,数组是否经过了特殊标记,以便在执行离开调用站点的范围时立即解除分配?
或者,也许是因为C#或JIT编译器知道参数的数量(当使用"原始"时),它能做与stackalloc
关键字相同的事情并将数组放在堆栈上,从而不需要解除分配吗?
是的,每次都会分配一个新的数组。
没有,没有进行任何优化。没有你建议的那种"实习"。毕竟,怎么会有呢?接收方法可以对数组执行任何操作,包括更改其成员,或重新分配数组条目,或将对数组的引用传递给其他人(然后不使用params
)。
不存在你建议的那种特殊的"标签"。这些数组以与其他任何数组相同的方式进行垃圾收集。
附加:当然,有一种特殊情况,我们在这里讨论的那种"实习"可能很容易做到,那就是长度为零的数组。每当遇到需要长度为零的数组的params
调用时,C#编译器可以调用Array.Empty<T>()
(每次返回相同长度的零数组),而不是创建new T[] { }
。
这种可能性的原因是长度为零的数组确实是不可变的。
当然,长度为零的数组的"内部化"是可以发现的,例如,如果引入以下功能,则此类的行为将发生变化:
class ParamsArrayKeeper
{
readonly HashSet<object[]> knownArrays = new HashSet<object[]>(); // reference-equals semantics
public void NewCall(params object[] arr)
{
var isNew = knownArrays.Add(arr);
Console.WriteLine("Was this params array seen before: " + !isNew);
Console.WriteLine("Number of instances now kept: " + knownArrays.Count);
}
}
附加:考虑到.NET的"奇怪"数组协方差不适用于值类型,你确定你的代码是吗
Int32[] values = new Int32[ 1, 2, 3 ];
String formatted1 = String.Format( CultureInfo.InvariantCulture, "{0} {1} {2}", values );
按预期工作(如果语法被更正为new[] { 1, 2, 3, }
或类似内容,这肯定会导致String.Format
过载错误)。
是的,每次调用都会分配一个新数组。
除了params
内联方法的复杂性(@PeterDuniho对此进行了解释)之外,还需要考虑以下几点:所有具有params
重载的性能关键型.NET方法都有只接受一个或多个参数的重载。如果自动优化是可能的,他们不会这么做。
-
Console
(也称为String
、TextWriter
、StringBuilder
等):public static void Write(String format, params Object[] arg)
public static void Write(String format, Object arg0)
public static void Write(String format, Object arg0, Object arg1)
public static void Write(bool value)
-
Array
:public unsafe static Array CreateInstance(Type elementType, params int[] lengths)
public unsafe static Array CreateInstance(Type elementType, int length)
public unsafe static Array CreateInstance(Type elementType, int length1, int length2)
public unsafe static Array CreateInstance(Type elementType, int length1, int length2, int length3)
-
Path
:public static String Combine(params String[] paths)
public static String Combine(String path1, String path2)
public static String Combine(String path1, String path2, String path3)
-
CancellationTokenSource
:public static CancellationTokenSource CreateLinkedTokenSource(params CancellationToken[] tokens)
public static CancellationTokenSource CreateLinkedTokenSource(CancellationToken token1, CancellationToken token2)
-
等等。
第页。S.我承认这并不能证明什么,因为优化可能已经在以后的版本中引入,但它仍然需要考虑。CancellationTokenSource
是在相对较新的4.0中引入的。
但是在官方的CLR运行时和JIT中,是否进行了任何优化来消除这种分配?
你得问问作者。但考虑到这需要付出多少努力,我对此表示怀疑。声明方法必须能够访问数组,并将使用数组语法检索成员。因此,任何优化都必须重写方法逻辑,将数组访问转换为直接参数访问。
此外,优化必须在全局范围内进行,同时考虑该方法的所有调用方。它还必须检测该方法是否将数组传递给其他任何对象。
这似乎不是一个可行的优化,尤其是考虑到它对运行时性能的贡献微乎其微。
或者,数组是否经过了特殊标记,以便在执行离开调用站点的范围时立即解除分配?
不需要对数组进行"特殊"标记,因为垃圾收集器已经可以很好地自动处理该场景。事实上,只要不再在声明方法中使用数组,就可以对其进行垃圾回收。无需等待方法返回。
编译器当前在方法调用之前创建一个新对象。不需要这样做,JITter可能会对其进行优化。
请参阅https://github.com/dotnet/roslyn/issues/36以讨论可能的更改和params的性能改进。