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

时间:2016-07-30 22:47:56

标签: c# .net variadic-functions

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

在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关键字做同样的事情并将数组放在堆栈上,因此不会需要解除分配吗?

4 个答案:

答案 0 :(得分:9)

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

不,没有进行任何优化。没有你建议的那种“实习”。毕竟,怎么会有?接收方法可以对数组执行任何操作,包括更改其成员,或重新分配数组条目,或将对数组的引用传递给其他人(然后没有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);
  }
}

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

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

按预期工作(如果语法更正为new[] { 1, 2, 3, }或类似,则肯定会转到String.Format的错误重载。

答案 1 :(得分:6)

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

除了使用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)

P上。 S.我承认它没有任何证据,因为优化可能已经在以后的版本中引入,但它仍然需要考虑。 CancellationTokenSource是在相对较新的4.0中引入的。

答案 2 :(得分:5)

  

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

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

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

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

  

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

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

答案 3 :(得分:2)

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

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