C# 中的 `params` 总是会在每次调用时分配一个新数组吗?
Will `params` in C# always cause a new array to be allocated on every call?
C#/.NET 通过传递 Array
类型的引用(与 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
关键字相同的事情并将数组放在堆栈上, 因此不需要释放它?
是的,每次都会分配一个新的数组。
不,没有进行任何优化。没有您建议的那种"interning"。毕竟,怎么可能呢?接收方法可以对数组做任何事情,包括改变它的成员,或者重新分配数组条目,或者将对数组的引用传递给其他人(没有 params
然后)。
不存在您建议的那种特殊 "tagging"。这些数组以与其他任何东西相同的方式被垃圾收集。
补充:当然有一种特殊情况,我们在这里讨论的那种 "interning" 很容易做到,那就是 长度为零的数组 . C# 编译器可以调用 Array.Empty<T>()
(每次 returns 相同的长度为零的数组),而不是在遇到对 params
的调用时创建 new T[] { }
,其中长度为 -需要零数组。
这种可能性的原因是长度为零的数组确实是不可变的。
当然 "interning" 个长度为零的数组是可以发现的,例如,如果要引入该功能,class 的行为将会改变:
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的"strange"数组协变不适用于值类型,你确定你的代码:
Int32[] values = new Int32[ 1, 2, 3 ];
String formatted1 = String.Format( CultureInfo.InvariantCulture, "{0} {1} {2}", values );
按预期工作(如果语法更正为 new[] { 1, 2, 3, }
或类似的语法,这肯定会导致 String.Format
的错误重载)。
But in the official CLR runtime and JIT are any optimizations done to eliminate this allocation?
你得问作者。但考虑到为此需要付出多少努力,我对此表示怀疑。声明方法必须能够访问数组,并将使用数组语法检索成员。因此,任何优化都必然会不得不重写方法逻辑以将数组访问转换为直接参数访问。
此外,优化必须在全局范围内进行,同时考虑到该方法的所有 个调用方。并且它必须检测该方法是否将数组传递给其他任何东西。
这似乎不是一个可行的优化,尤其是考虑到它对运行时性能的贡献微乎其微。
Or perhaps is the array tagged specially so that it will be deallocated as soon as execution leaves the scope of the call-site?
不需要标记数组"specially",因为垃圾收集器已经自动很好地处理了这种情况。事实上,只要声明方法中不再使用该数组,就可以对其进行垃圾回收。无需等到方法 returns.
是的,每次调用都会分配一个新数组。
除了使用 params
的内联方法的复杂性之外,@PeterDuniho 对此进行了解释,请考虑以下几点:所有具有 [=10= 的性能关键 .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 中引入的。
编译器当前在方法调用之前创建了一个新对象。不需要这样做,JITter 可能会将其优化掉。
请参阅 https://github.com/dotnet/roslyn/issues/36 以了解有关使用参数进行可能的更改性能改进的讨论。
C#/.NET 通过传递 Array
类型的引用(与 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
关键字相同的事情并将数组放在堆栈上, 因此不需要释放它?
是的,每次都会分配一个新的数组。
不,没有进行任何优化。没有您建议的那种"interning"。毕竟,怎么可能呢?接收方法可以对数组做任何事情,包括改变它的成员,或者重新分配数组条目,或者将对数组的引用传递给其他人(没有 params
然后)。
不存在您建议的那种特殊 "tagging"。这些数组以与其他任何东西相同的方式被垃圾收集。
补充:当然有一种特殊情况,我们在这里讨论的那种 "interning" 很容易做到,那就是 长度为零的数组 . C# 编译器可以调用 Array.Empty<T>()
(每次 returns 相同的长度为零的数组),而不是在遇到对 params
的调用时创建 new T[] { }
,其中长度为 -需要零数组。
这种可能性的原因是长度为零的数组确实是不可变的。
当然 "interning" 个长度为零的数组是可以发现的,例如,如果要引入该功能,class 的行为将会改变:
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的"strange"数组协变不适用于值类型,你确定你的代码:
Int32[] values = new Int32[ 1, 2, 3 ];
String formatted1 = String.Format( CultureInfo.InvariantCulture, "{0} {1} {2}", values );
按预期工作(如果语法更正为 new[] { 1, 2, 3, }
或类似的语法,这肯定会导致 String.Format
的错误重载)。
But in the official CLR runtime and JIT are any optimizations done to eliminate this allocation?
你得问作者。但考虑到为此需要付出多少努力,我对此表示怀疑。声明方法必须能够访问数组,并将使用数组语法检索成员。因此,任何优化都必然会不得不重写方法逻辑以将数组访问转换为直接参数访问。
此外,优化必须在全局范围内进行,同时考虑到该方法的所有 个调用方。并且它必须检测该方法是否将数组传递给其他任何东西。
这似乎不是一个可行的优化,尤其是考虑到它对运行时性能的贡献微乎其微。
Or perhaps is the array tagged specially so that it will be deallocated as soon as execution leaves the scope of the call-site?
不需要标记数组"specially",因为垃圾收集器已经自动很好地处理了这种情况。事实上,只要声明方法中不再使用该数组,就可以对其进行垃圾回收。无需等到方法 returns.
是的,每次调用都会分配一个新数组。
除了使用 params
的内联方法的复杂性之外,@PeterDuniho 对此进行了解释,请考虑以下几点:所有具有 [=10= 的性能关键 .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 中引入的。
编译器当前在方法调用之前创建了一个新对象。不需要这样做,JITter 可能会将其优化掉。
请参阅 https://github.com/dotnet/roslyn/issues/36 以了解有关使用参数进行可能的更改性能改进的讨论。