2012-08-03 3 views
24

Компилятор .NET C# (.NET 4.0) компилирует оператор fixed довольно своеобразным способом.Почему MSFT C# компилирует исправленный «массив в размытие указателя» и «адрес первого элемента» по-другому?

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

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/небезопасного/о +", если вы хотите следовать.

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

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:

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. Если буфер пустой, GOTO 3.
  2. Если buffer.Length = 0, GOTO 5.
  3. хранить значение 0 в местный! slot 0,
  4. GOTO 6.
  5. Получить адрес буфера [0].
  6. Относитесь к этому адресу (в локальном слоте 0, который может быть 0 или буфером сейчас).
  7. Вызовите WriteLine с этим разыменованным значением.

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

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

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

Обработка null таким образом имеет то преимущество, что объектная модель в C# соответствует. То есть внутри блока fixedp по-прежнему обрабатывается семантически как своего рода «указатель на управляемый массив», который не будет, когда null, вызывает проблемы до тех пор, пока (или если) не будет разыменован. Согласованность - все хорошо и хорошо, но проблема в том, что 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) говорит, что поведение «Реализация определенных» в обоих случаях отказа:

Фиксированный-указатель-инициализатор может быть один из следующее:

• Токен «&», за которым следует переменная-ссылка (§5.3.3) на подвижную переменную (§18.3) неуправляемого типа T, если тип T * неявно конвертируется в указатель тип, заданный в фиксированном утверждении. В этом случае инициализатор вычисляет адрес данной переменной, и гарантируется, что переменная останется на фиксированном адресе на протяжении фиксированного оператора.

• Выражение типа массива с элементами неуправляемого типа T, если тип T * неявно конвертируется в тип указателя, заданный в фиксированном утверждении. В этом случае инициализатор вычисляет адрес первого элемента в массиве, и весь массив, как гарантируется, останется на фиксированном адресе на протяжении фиксированного оператора. Поведение фиксированного оператора определяется реализацией, если выражение массива равно null или массив имеет нулевые элементы.

... другие случаи ...

Последняя точка зрения, MSDN documentation предполагает, что два "эквивалент":

// Следующие два задания эквивалентны ...

фиксированной (двойной * р = обр) {/ ... /}

фиксированных (двойной * р = & обр [0]) {/ ... /}

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

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

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

+0

@pst Я удалил 'SoWhat' и его объяснение для краткость. Исправлена. –

+1

Вы проверили, что говорит спецификация C# о 'arr [0]'? Я уверен, что 'arr [0]' генерирует исключение, когда 'arr' является нулевым или пустым в любом контексте, независимо от какого-либо' фиксированного' утверждения, которое его окружает. – hvd

+0

@hvd Да, поэтому 'fixed (byte * p = buffer)' следует просто рассматривать как 'fixed (byte * p = & buffer [0])'. Последний обеспечивает более четкую проверку ошибок. Я добавлю это к вопросу. –

ответ

9

Вы могли заметить, что код IL, который вы включили, реализует спецификацию почти линейно-для-линии. Это включает в себя явно реализацию двух исключений, перечисленных в спецификации, в случае, когда они являются релевантными, и не, включая код в случае, если они не являются. Итак, самая простая причина, по которой компилятор ведет себя так, как он делает, - «потому что спецификация так говорила».

Конечно, это как раз и приводит к еще двух вопросов, которые мы могли бы спросить:

  • Почему группа # языка C решили написать спецификацию таким образом?
  • Почему команда компилятора выбрала именно то, что конкретно поведение, определяемое реализацией?

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

Напомним, что спецификация говорит, в случае подачи массива к фиксированной указателю-инициализаторе, что

Поведение фиксированного утверждения определяется реализацией, если выражение массива null или если массив имеет нулевые элементы.

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

В этом случае команда компилятора решила сделать это: «сделать исключение в том месте, где ваш код делает что-то неправильно». Посмотрите, что будет делать код, если бы он не был внутри инициализатора фиксированного указателя и подумал о том, что еще происходит. В примере «Хороший» вы пытаетесь взять адрес объекта, который не существует: первый элемент в пустом/пустом массиве. Это не то, что вы действительно можете сделать, так что это приведет к исключению. В примере «Плохой» вы просто назначаете адрес параметра переменной указателя; byte * p = null - совершенно законное утверждение. Только при попытке WriteLine(*p) произошла ошибка. Поскольку фиксированный указатель-инициализатор имеет право делать все, что он хочет в этом случае исключения, самое простое дело - просто разрешить выполнение задания, как ничтожно.

Очевидно, что два оператора: не точно эквивалент. Мы можем сказать, что это тот факт, что стандарт относится к ним по-разному:

  • &arr[0] является: «Маркер„&“, а затем переменной ссылки», и поэтому компилятор вычисляет адрес обр [0]
  • arr: «выражение типа массива», и поэтому компилятор вычисляет адрес первого элемента массива с предостережением, что массив с нулевой или нулевой длиной создает поведение, определяемое реализацией, которое вы видите.

Эти две эквивалентные результаты, до тех пор, пока элемент в массиве, который является точкой, что документация MSDN пытается донести. Задавая вопросы о том, почему явно неопределенное или определенное реализацией поведение действует так, как это делается, на самом деле не поможет вам решить какие-либо конкретные проблемы, потому что вы не можете полагаться на это, чтобы быть правдой в будущем. (Сказав это, мне, конечно, было бы интересно узнать, что такое процесс мышления, поскольку вы, очевидно, не можете «зафиксировать» нулевое значение в памяти ...)

+0

В соответствии с реализацией реализации спецификации допускается только во втором случае, но Good реализует первый случай. '& array [0]' is * not * выражение массива. – usr

+1

«Нам нужно надеяться, что кто-то из команды компилятора C# произойдет« Это моя надежда :) –

+0

@usr true, я пытался получить эту точку в сообщении, но я буду более откровенен в этом :) –

1

Таким образом, мы видим, что хорошие и плохие семантически разные. Зачем?

Поскольку Благ случай 1 и плохо дело 2.

Хорошо не назначает «выражение массива типа». Он назначает «токен» & «за которым следует ссылка на переменную», так что это случай 1. Плохо присваивает «выражение типа массива», делающее это случай 2. Если это так, документация MSDN неверна.

В любом случае это объясняет, почему компилятор C# создает два разных (и во втором случае специализированных) шаблонов кода.

Почему случай 1 генерирует такой простой код? Я размышляю здесь: Принимая адрес элемента массива, вероятно, скомпилирован так же, как с использованием array[index] в выражении ref. На уровне CLR параметры и выражения ref являются управляемыми указателями. Так выглядит выражение &array[index]: оно скомпилировано управляемым указателем, который не закреплен, а «внутренним» (этот термин исходит из Managed C++, я думаю). GC исправляет это автоматически. Он ведет себя как нормальная ссылка на объект.

Итак, случай 1 получает обычную обработку управляемого указателя, в то время как случай 2 получает специальное, определенное по реализации (не неопределенное) поведение.

Это не отвечает на все ваши вопросы, но, по крайней мере, это дает некоторые причины для ваших наблюдений. Я как бы надеялся, что Эрик Липперт добавит свой ответ в качестве инсайдера.

+2

«Если это так, документация MSDN неверна». На самом деле, я просто не понимаю, как я понимаю. Эти две формы эквивалентны для тех буферов, для которых обе формы определили поведение. – hvd

+0

@ hvd, да для этих случаев. Но не для всех. Документация не соответствует его заявлению, как вы это делали. – usr

+0

@hvd Я должен ожидать, что «поведение, определяемое реализацией», определено в документах MSDN. Понятно, что это не так. –