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
关键字做同样的事情并将数组放在堆栈上,因此不会需要解除分配吗?
答案 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
(还有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)
等
P上。 S.我承认它没有任何证据,因为优化可能已经在以后的版本中引入,但它仍然需要考虑。 CancellationTokenSource
是在相对较新的4.0中引入的。
答案 2 :(得分:5)
但是在正式的CLR运行时和JIT中是否进行了任何优化来消除这种分配?
你必须问作者。但考虑到需要付出多少努力,我对此表示怀疑。声明方法必须能够访问数组,并使用数组语法检索成员。因此,任何优化都必须重写方法逻辑以将数组访问转换为直接参数访问。
此外,优化必须在全球范围内进行,并考虑方法的所有调用者。它必须检测方法是否将数组传递给其他任何东西。
这似乎不是一个可行的优化,特别是考虑到它会给运行时性能增加多少价值。
或许是特殊标记的数组,以便一旦执行离开调用站点的范围就会解除分配?
没有必要“特别”标记数组,因为垃圾收集器已经自动处理好了。实际上,只要在声明方法中不再使用该数组,就可以对其进行垃圾回收。无需等到方法返回。
答案 3 :(得分:2)
编译器当前在方法调用之前创建一个新对象。不需要这样做,JITter可能会对其进行优化。
有关params可能改变性能改进的讨论,请参阅https://github.com/dotnet/roslyn/issues/36。