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

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

这意味着生成的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编译器知道参数的数量(当使用“raw”时)它可以做与stackalloc关键字相同的事情并将数组放在堆栈上,因此不需要解除分配吗?

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

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

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


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

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

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

 class ParamsArrayKeeper { readonly HashSet knownArrays = new HashSet(); // 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的错误重载,当然)。

是的,每次通话都会分配一个新arrays。

除了使用params的内联方法的复杂性(由@PeterDuniho解释) 之外 ,请考虑这一点:所有具有params重载的性能关键的.NET方法都有重载只带一个或多个参数。 如果可以进行自动优化,他们就不会这样做。

  • Console (也是StringTextWriterStringBuilder等):

    • 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中引入的。

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

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

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

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

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

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

编译器当前在方法调用之前创建一个新对象。 不需要这样做,JITter可能会优化它。

有关使用params可能改变性能改进的讨论,请参阅https://github.com/dotnet/roslyn/issues/36 。