Will `params` in C# always cause a new array to be allocated on every call?

扶醉桌前 提交于 2019-12-06 17:55:16

问题


C#/.NET has variadic function parameters by passing an Array type by-reference (as opposed to C/C++ which just places all of the values directly on the stack, for better and for worse).

In the C# world this has a neat advantage of allowing you to call the same function with either 'raw' arguments or a reusable array instance:

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

This means that the generated CIL is equivalent to:

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

Which means that (in a non-optimizing JIT compiler) every call will allocate a new Object[] instance - though in the third example you're able to store the array as a field or other reusable value to eliminate the new allocation on every call to 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?

Or, perhaps, because the C# or JIT compiler knows the number of arguments (when used "raw") could it do the same thing as the stackalloc keyword and place the array on the stack, and thus not need to deallocate it?


回答1:


Yes, a new array is allocated every time.

No, no optimizations are done. There is no "interning" of the kind you suggest. After all, how could there be? The receiving method can do anything with the array, including mutating its members, or reassigning array entries, or passing a reference to the array on to others (without params then).

No special "tagging" of the kind you suggest, exists. These arrays are garbage collected in the same way as anything else.


Addition: There is of course one special case where "interning" of the kind we discuss here, could be easy to do, and that is for length-zero arrays. The C# compiler could call Array.Empty<T>() (which returns the same length-zero array each time) instead of creating a new T[] { } whenever it encountered a call to params where a length-zero array is needed.

The reason for this possibility is that length-zero arrays are truly immutable.

Of course "interning" of length-zero arrays would be discoverable, for example the behavior of this class would change if the feature were to be introduced:

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

Addition: Given that the "strange" array covariance of .NET does not apply to value types, are you sure your code:

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

works as intended (if the syntax is corrected to new[] { 1, 2, 3, } or similar, this will go to the wrong overload of String.Format, for sure).




回答2:


Yes, a new array will be allocated on every call.

Besides complexity of inlining methods with params, which was explained by @PeterDuniho, consider this: all performance-critical .NET methods which have params overloads have overloads taking just one or several arguments too. They wouldn't do this if automatic optimization was possible.

  • Console (also String, TextWriter, StringBuilder etc.):

    • 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)
  • etc.

P. S. I admit it doesn't prove anything, as optimization may have been introduced in later versions, but it's still something to consider. CancellationTokenSource was introduced in relatively recent 4.0.




回答3:


But in the official CLR runtime and JIT are any optimizations done to eliminate this allocation?

You'd have to ask the authors. But given how much effort would have to go into that, I doubt it. The declaring method has to have access to the array, and will retrieve members using array syntax. So any optimization would necessarily wind up having to rewrite the method logic to translate array access to direct parameter access.

Furthermore, the optimization would have to take place globally, taking into account all callers of the method. And it would have to detect whether the method ever passes the array to anything else.

This just doesn't seem like a feasible optimization, especially given how little value it would add to runtime performance.

Or perhaps is the array tagged specially so that it will be deallocated as soon as execution leaves the scope of the call-site?

There's no need to tag the array "specially", as the garbage collector already will automatically handle the scenario well. In fact, the array can be garbage collected as soon as it's no longer used in the declaring method. No need to wait until the method returns.




回答4:


The compiler currently creates a new object prior to the method call. It is not required to do so, and it is possible that the JITter optimizes it away.

See https://github.com/dotnet/roslyn/issues/36 for a discussion on possible changes performance improvements with params.



来源:https://stackoverflow.com/questions/38679180/will-params-in-c-sharp-always-cause-a-new-array-to-be-allocated-on-every-call

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!