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

C#中的Will‘params’总是会在每次调用时分配一个新数组

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

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

第页。S.我承认这并不能证明什么,因为优化可能已经在以后的版本中引入,但它仍然需要考虑。CancellationTokenSource是在相对较新的4.0中引入的。

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

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

此外,优化必须在全局范围内进行,同时考虑该方法的所有调用方。它还必须检测该方法是否将数组传递给其他任何对象。

这似乎不是一个可行的优化,尤其是考虑到它对运行时性能的贡献微乎其微。

或者,数组是否经过了特殊标记,以便在执行离开调用站点的范围时立即解除分配?

不需要对数组进行"特殊"标记,因为垃圾收集器已经可以很好地自动处理该场景。事实上,只要不再在声明方法中使用数组,就可以对其进行垃圾回收。无需等待方法返回。

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

请参阅https://github.com/dotnet/roslyn/issues/36以讨论可能的更改和params的性能改进。