C#中的`params`会不会导致每次调用都分配一个新的数组?

Dai*_*Dai 14 .net c# variadic-functions

C#/ .NET通过传递Array类型by-reference来具有可变参数函数参数(与C/C++相反,它只是将所有值直接放在堆栈上,无论好坏).

在C#世界中,这有一个很好的优点,允许您使用'raw'参数或可重用的数组实例调用相同的函数:

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 );
Run Code Online (Sandbox Code Playgroud)

这意味着生成的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 );
Run Code Online (Sandbox Code Playgroud)

这意味着(在非优化JIT编译器中)每个调用都将分配一个新Object[]实例 - 尽管在第三个示例中,您可以将数组存储为字段或其他可重用值,以消除每次调用时的新分配String.Format.

但是在正式的CLR运行时和JIT中是否进行了任何优化来消除这种分配?或者也许是特殊标记的数组,以便一旦执行离开调用站点的范围就会解除分配?

或者,也许,因为C#或JIT编译器知道参数的数量(当使用"raw"时)它可以做与stackalloc关键字相同的事情并将数组放在堆栈上,因此不需要解除分配吗?

Jep*_*sen 10

是的,每次都会分配一个新数组.

不,没有做出任何优化.没有你建议的那种"实习".毕竟,怎么会有?接收方法可以对数组执行任何操作,包括更改其成员,或重新分配数组条目,或将对数组的引用传递给其他人(不需要params).

没有特别的"标记",你建议的那种,存在.这些数组以与其他任何方式相同的方式进行垃圾收集.


另外:当然有一个特殊情况,我们在这里讨论的那种"实习"可能很容易做到,那就是长度为零的数组.C#编译器可以调用Array.Empty<T>()(每次都返回相同的长度为零的数组),而不是new T[] { }在遇到params需要长度为零的数组的调用时创建.

这种可能性的原因是长度为零的数组是真正不可变的.

当然,长度为零的数组的"实习"是可发现的,例如,如果要引入该特征,该类的行为将会改变:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

另外:鉴于.NET的"奇怪"数组协方差不适用于值类型,您确定您的代码:

Int32[] values = new Int32[ 1, 2, 3 ];
String formatted1 = String.Format( CultureInfo.InvariantCulture, "{0} {1} {2}", values );
Run Code Online (Sandbox Code Playgroud)

按预期工作(如果语法被修正为new[] { 1, 2, 3, }或类似,这将导致错误的重载String.Format,当然).


Ath*_*ari 7

是的,每次通话都会分配一个新阵列.

除了内联方法的复杂性,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)
  • 等等

PS我承认它没有任何证据,因为优化可能已经在以后的版本中引入,但它仍然需要考虑.CancellationTokenSource是在最近的4.0中引入的.


Pet*_*iho 5

但是在正式的CLR运行时和JIT中是否进行了任何优化来消除这种分配?

你必须问作者.但考虑到需要付出多少努力,我对此表示怀疑.声明方法必须能够访问数组,并使用数组语法检索成员.因此,任何优化都必然需要重写方法逻辑以将数组访问转换为直接参数访问.

此外,考虑到该方法的所有呼叫者,优化必须全局进行.它必须检测方法是否将数组传递给其他任何东西.

这似乎不是一个可行的优化,特别是考虑到它将增加运行时性能的价值.

或者也许是特殊标记的数组,以便一旦执行离开调用站点的范围就会解除分配?

没有必要"特别"标记数组,因为垃圾收集器已经自动处理好了.实际上,只要在声明方法中不再使用该数组,就可以对其进行垃圾回收.无需等到方法返回.