Почему MSFT C #по-разному компилирует фиксированный «массив в распад указателя» и «адрес первого элемента»?

Компилятор.NET c #(.NET 4.0 )довольно своеобразно компилирует оператор fixed.

Вот короткая, но полная программа, чтобы показать вам, о чем я говорю.

using System;

public static class FixedExample {

    public static void Main() {
        byte [] nonempty = new byte[1] {42};
        byte [] empty = new byte[0];

        Good(nonempty);
        Bad(nonempty);

        try {
            Good(empty);
        } catch (Exception e){
            Console.WriteLine(e.ToString());
            /* continue with next example */
        }
        Console.WriteLine();
        try {
            Bad(empty);
        } catch (Exception e){
            Console.WriteLine(e.ToString());
            /* continue with next example */
        }
     }

    public static void Good(byte[] buffer) {
        unsafe {
            fixed (byte * p = &buffer[0]) {
                Console.WriteLine(*p);
            }
        }
    }

    public static void Bad(byte[] buffer) {
        unsafe {
            fixed (byte * p = buffer) {
                Console.WriteLine(*p);
            }
        }
    }
}

Скомпилируйте его с помощью «csc.exe FixedExample.cs /unsafe /o+», если хотите следовать дальше.

Вот сгенерированный IL для методаGood:

Хорошо()

 .maxstack  2
 .locals init (uint8& pinned V_0)
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.0
  IL_0002:  ldelema    [mscorlib]System.Byte
  IL_0007:  stloc.0
  IL_0008:  ldloc.0
  IL_0009:  conv.i
  IL_000a:  ldind.u1
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_0010:  ldc.i4.0
  IL_0011:  conv.u
  IL_0012:  stloc.0
  IL_0013:  ret

Вот сгенерированный IL для методаBad:

Плохо()

 .locals init (uint8& pinned V_0, uint8[] V_1)
  IL_0000:  ldarg.0
  IL_0001:  dup
  IL_0002:  stloc.1
  IL_0003:  brfalse.s  IL_000a
  IL_0005:  ldloc.1
  IL_0006:  ldlen
  IL_0007:  conv.i4
  IL_0008:  brtrue.s   IL_000f
  IL_000a:  ldc.i4.0
  IL_000b:  conv.u
  IL_000c:  stloc.0
  IL_000d:  br.s       IL_0017
  IL_000f:  ldloc.1
  IL_0010:  ldc.i4.0
  IL_0011:  ldelema    [mscorlib]System.Byte
  IL_0016:  stloc.0
  IL_0017:  ldloc.0
  IL_0018:  conv.i
  IL_0019:  ldind.u1
  IL_001a:  call       void [mscorlib]System.Console::WriteLine(int32)
  IL_001f:  ldc.i4.0
  IL_0020:  conv.u
  IL_0021:  stloc.0
  IL_0022:  ret

Вот что делает Good:

  1. Получить адрес буфера[0].
  2. Разыменуйте этот адрес.
  3. Вызовите WriteLine с этим разыменованным значением.

Вот что делает Bad `:

  1. Если буфер пуст, ПЕРЕЙТИ К 3.
  2. Если buffer.Length != 0, ПЕРЕЙТИ К 5.
  3. Сохраните значение 0 в локальном слоте 0,
  4. ПЕРЕЙТИ К 6.
  5. Получить адрес буфера[0].
  6. Уважайте этот адрес (в локальном слоте 0, который может быть 0 или буфером сейчас ).
  7. Вызовите WriteLine с этим разыменованным значением.

Когда bufferи не -null, и не -пусто, эти две функции делают одно и то же. Обратите внимание, что Badпросто перескакивает через несколько обручей, прежде чем перейти к вызову функции WriteLine.

Когда bufferравно нулю, Goodвыдает NullReferenceExceptionв декларатор фиксированного -указателя (byte * p = &buffer[0]).. Предположительно, это желаемое поведение для исправления управляемого массива, потому что в целом любая операция внутри фиксированного -оператора будет зависеть от достоверности фиксируемого объекта. Иначе зачем этот код находиться внутри блока fixed? Когда Goodпередается нулевая ссылка, происходит сбой сразу же в начале блока fixed, обеспечивая релевантную и информативную трассировку стека. Разработчик увидит это и поймет, что он должен проверить bufferперед его использованием, или, возможно, его логика неправильно назначила nullна buffer. В любом случае, явный вход в блок fixedс управляемым массивом nullнежелателен.

Badобрабатывает этот случай по-разному, даже нежелательно. Вы можете видеть, что Badна самом деле не генерирует исключение, пока pне будет разыменован. Он делает это окольным путем: присваивает null тому же локальному слоту, который содержит p, а затем выдает исключение, когда операторы блока fixedразыменовывают p.

Преимущество такой обработки nullзаключается в сохранении согласованности объектной модели в C #. То есть внутри блока fixedpпо-прежнему семантически обрабатывается как своего рода «указатель на управляемый массив», который при нулевом значении не будет вызывать проблем до (или до тех пор, пока )не будет разыменован. Согласованность — это хорошо, но проблема в том, что p не является указателем на управляемый массив .Это указатель на первый элемент buffer, и любой, кто написал этот код (Bad), будет интерпретировать его семантическое значение как таковое. Вы не можете получить размер bufferиз pи не можете вызвать p.ToString(), так зачем обращаться с ним, как с объектом? В тех случаях, когда bufferимеет значение null, явно имеет место ошибка кодирования, и я считаю, что было бы гораздо полезнее, если бы Badвыбрасывал исключение в декларатор фиксированного -указателя , а не внутри метода.

Таким образом, кажется, что Goodобрабатывает nullлучше, чем Bad. Что делать с пустыми буферами?

Когда bufferимеет длину 0, Goodвыдает IndexOutOfRangeExceptionв фиксированный -декларатор указателя . Это кажется вполне разумным способом обработки доступа к массиву за пределами границ. В конце концов, код &buffer[0]следует обрабатывать так же, как &(buffer[0]), который, очевидно, должен выдавать IndexOutOfRangeException.

Badобрабатывает этот случай по-разному, и снова нежелательно. Точно так же, как если бы bufferбыло null, когда buffer.Length == 0, Badне выдает исключение до тех пор, пока pне будет разыменовано, и в это время выбрасывает NullReferenceException, а не IndexOutOfRangeException! Если pникогда не разыменовывается, код даже не выдает исключение. Опять же, кажется, что идея здесь состоит в том, чтобы дать pсемантическое значение «указатель на управляемый массив». Опять же, я не думаю, что кто-либо, пишущий этот код, будет думать о pтаким образом. Код был бы намного полезнее, если бы он бросал IndexOutOfRangeExceptionв декларатор фиксированного -указателя , тем самым уведомляя разработчика о том, что переданный массив пуст, а не null.

Похоже, fixed(byte * p = buffer)должен был быть скомпилирован в тот же код, что и fixed (byte * p = &buffer[0]). Также обратите внимание, что хотя bufferмогло быть любым произвольным выражением, его тип(byte[])известно во время компиляции, поэтому код в Goodбудет работать для любого произвольного выражения.

Изменить

Фактически,обратите внимание, что реализация Badфактически выполняет проверку ошибок наbuffer[0]дважды . Он делает это явно в начале метода, а затем снова неявно в инструкции ldelema.


Итак, мы видим, что Goodи Badсемантически различны. Badдлиннее, возможно, медленнее и, конечно же, не дает нам желаемых исключений, когда у нас есть ошибки в нашем коде, и даже в некоторых случаях дает сбой намного позже, чем должен.

Для тех, кому любопытно, в разделе 18.6 спецификации (C #4.0 )говорится, что поведение «определяется реализацией -» в обоих этих случаях отказа :

A fixed-pointer-initializer can be one of the following:

• The token “&” followed by a variable-reference (§5.3.3) to a moveable variable (§18.3) of an unmanaged type T, provided the type T* is implicitly convertible to the pointer type given in the fixed statement. In this case, the initializer computes the address of the given variable, and the variable is guaranteed to remain at a fixed address for the duration of the fixed statement.

• An expression of an array-type with elements of an unmanaged type T, provided the type T* is implicitly convertible to the pointer type given in the fixed statement. In this case, the initializer computes the address of the first element in the array, and the entire array is guaranteed to remain at a fixed address for the duration of the fixed statement. The behavior of the fixed statement is implementation-defined if the array expression is null or if the array has zero elements.

... other cases...

. Наконец, документация MSDN предполагает, что эти два понятия «эквивалентны» :

// The following two assignments are equivalent...

fixed (double* p = arr) { /.../ }

fixed (double* p = &arr[0]) { /.../ }

. Если предполагается, что они «эквивалентны», то зачем использовать различную семантику обработки ошибок для первого оператора?

Также кажется, что дополнительные усилия были приложены к написанию путей кода, сгенерированных в Bad. Скомпилированный код в Goodотлично работает для всех случаев отказа и аналогичен коду в Badв случаях, отличных от -. Зачем реализовывать новые пути кода, а не просто использовать более простой код, сгенерированный для Good?

Почему это реализовано именно так?

25
задан Michael Graczyk 3 August 2012 в 22:28
поделиться