Будет ли `params` в С# всегда вызывать новый массив для каждого вызова?

С#/.NET имеет параметры вариационной функции, передавая по типу Array тип-ссылку (в отличие от 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 есть какие-либо оптимизации для устранения этого распределения? Или, может быть, массив помечен специально, чтобы он был освобожден, как только выполнение оставит область вызова сайта?

Или, может быть, потому, что компилятор С# или JIT знает количество аргументов (при использовании "raw" ), он мог бы сделать то же самое, что и ключевое слово stackalloc, и поместить массив в стек, и, следовательно, не нужно освободить его?

Ответ 1

Да, каждый раз выделяется новый массив.

Нет, оптимизация не выполняется. Не существует "интернирования" такого рода, который вы предлагаете. В конце концов, как это могло быть? Метод приема может делать что-либо с массивом, включая мутирование его членов или переназначение записей в массиве, или передачу ссылки на массив на другие (без params then).

Никакой специальной "маркировки" того типа, который вы предлагаете, не существует. Эти массивы собирают мусор таким же образом, как и все остальное.


Дополнение: Конечно, есть один специальный случай, когда "интернирование" такого рода, которое мы обсуждаем здесь, может быть легко выполнено, и это для массивов с нулевой длиной. Компилятор С# может вызывать 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).

Ответ 2

Да, при каждом вызове будет выделен новый массив.

Помимо сложности методов наложения с помощью params, который был объяснен @PeterDuniho, рассмотрите это: все критически важные для .NET методы, имеющие перегрузки params, имеют перегрузки, принимающие только один или несколько аргументы тоже. Они не сделали бы этого, если бы была возможна автоматическая оптимизация.

  • 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)
  • и др.

Р. С. Я признаю, что это ничего не доказывает, поскольку оптимизация, возможно, была введена в более поздних версиях, но это все еще нужно рассмотреть. CancellationTokenSource был представлен в сравнительно недавнем 4.0.

Ответ 3

Но в официальной среде CLR и JIT есть какие-либо оптимизации для устранения этого распределения?

Вам придется спросить авторов. Но, учитывая, как много усилий придется вникать в это, я сомневаюсь. Метод объявления должен иметь доступ к массиву и извлекать элементы, используя синтаксис массива. Поэтому любая оптимизация обязательно заставит переписать логику метода для преобразования доступа к массиву для прямого доступа к параметрам.

Кроме того, оптимизация должна проводиться глобально с учетом всех вызывающих методов. И он должен был бы определить, когда метод когда-либо передал массив на что-нибудь еще.

Это просто не похоже на возможную оптимизацию, особенно учитывая, насколько малое значение она добавит к производительности выполнения.

Или, может быть, массив помечен специально для того, чтобы он был освобожден, как только выполнение выйдет из области вызова?

Нет необходимости отмечать массив "специально", так как сборщик мусора уже автоматически обработает сценарий. Фактически, массив может быть собран в мусор, как только он больше не используется в методе декларирования. Не нужно ждать, пока метод вернется.

Ответ 4

В настоящее время компилятор создает новый объект перед вызовом метода. Это не требуется, и, возможно, JITter оптимизирует его.

См. https://github.com/dotnet/roslyn/issues/36 для обсуждения возможных изменений производительности с параметрами.