Константы и оценка времени компиляции - зачем менять это поведение - PullRequest
7 голосов
/ 24 января 2012

Если вы отправите приблизительно 13 минут в это видео Эрика Липперта, он описывает изменение, которое было внесено в компилятор C #, из-за которого следующий код становится недействительным (очевидно, до и после .NET 2 этот код бы скомпилировал).

int y;
int x = 10;
if (x * 0 == 0)
    y = 123;

Console.Write(y);

Теперь я понимаю, что любое выполнение приведенного выше кода действительно оценивается как

.
int y;
int x = 10;
y = 123;
Console.Write(y);

Но что я не понимаю, так это почему "желательно" сделать следующий код некомпилируемым? IE: Каковы риски, позволяющие таким умозаключениям идти своим чередом?

Ответы [ 2 ]

8 голосов
/ 24 января 2012

Я все еще нахожу этот вопрос немного запутанным, но позвольте мне посмотреть, могу ли я перефразировать вопрос в форму, на которую я могу ответить. Во-первых, позвольте мне перефразировать фон вопроса:

В C # 2.0 этот код:

int x = 123;
int y;
if (x * 0 == 0) 
    y = 345;
Console.WriteLine(y);

трактовался так, как если бы вы написали

int x = 123;
int y;
if (true) 
    y = 345;
Console.WriteLine(y);

, который в свою очередь рассматривается как:

int x = 123;
int y;
y = 345;
Console.WriteLine(y);

Какая легальная программа.

Но в C # 3.0 мы приняли решающее изменение, чтобы предотвратить это. Компилятор больше не рассматривает условие как «всегда истинное», несмотря на то, что мы оба знаем, что оно всегда верно. Теперь мы делаем это недопустимой программой, потому что компилятор считает, что он не знает , что тело «if» всегда выполняется, и поэтому не знает, что перед ним всегда назначается локальная переменная y используется.

Почему корректно поведение C # 3.0?

Это правильно, потому что в спецификации говорится:

  • константное выражение должно содержать только константы. x * 0 == 0 не является константным выражением, поскольку оно содержит непостоянный член, x.

  • известно, что следствие if всегда достижимо, только если условие является константным выражением, равным true.

Следовательно, данный код не должен классифицировать последствия того, что условный оператор будет всегда доступен, и, следовательно, не должен классифицировать локальный y как однозначно назначенный.

Почему желательно, чтобы константное выражение содержало только константы?

Мы хотим, чтобы язык C # был понятным для его пользователей и корректно реализовывался авторами компиляторов. Требование, чтобы компилятор делал все возможные логические выводы о значениях выражений, работает против этих целей. Должно быть simple , чтобы определить, является ли данное выражение константой, и если да, каково его значение. Проще говоря, код константной оценки должен знать как выполнять арифметику, но не должен знать факты об арифметических манипуляциях. Оценщик констант знает как умножить 2 * 1, но ему не нужно знать факт , что «1 - это мультипликативная единица на целых числах».

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

Как произошла ошибка в C # 2.0?

Произошло то, что компилятор был написан для слишком раннего запуска арифметического оптимизатора. Оптимизатор - это бит, который должен быть умным, и он должен был запустить после , если программа была определена как допустимая. Она работала до , программа была признана законной, и, следовательно, влияла на результат.

Это было потенциальное критическое изменение: хотя оно и привело компилятор в соответствие со спецификацией, оно также потенциально превратило рабочий код в код ошибки. Что послужило причиной изменения?

Функции LINQ и, в частности, деревья выражений. Если вы сказали что-то вроде:

(int x)=>x * 0 == 0

и преобразовал это в дерево выражений, ожидаете ли вы, что оно сгенерирует дерево выражений для

(int x)=>true

? Возможно нет! Вы, вероятно, ожидали, что он создаст дерево выражений для «умножения х на ноль и сравнения результата с нолем». Деревья выражений должны сохранять логическую структуру выражения в теле.

Когда я писал код дерева выражений, еще не было ясно, собирается ли проектная комиссия решать,

()=>2 + 3

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

Итакдавайте теперь рассмотрим зависимости, которые мы только что указали:

  • Арифметическая оптимизация должна произойти до Codegen.
  • Переписывание дерева выражений должно произойти до арифметической оптимизации
  • Свертывание констант должно произойти до переписывания дерева выражений
  • Свертывание констант должно произойти до анализа потока
  • Анализ потока должен произойти до переписывания дерева выражений (потому что нам нужно знать, использует ли дерево выраженийнеинициализированный локальный)

Мы должны найти приказ выполнить всю эту работу, которая учитывает все эти зависимости.Компилятор в C # 2.0 делал их в следующем порядке:

  • постоянное свертывание и арифметическая оптимизация одновременно
  • анализ потока
  • codegen

Куда может пойти переписывание дерева выражений?Нигде!И очевидно, что это ошибка, потому что анализ потока теперь учитывает факты, полученные арифметическим оптимизатором.Мы решили переработать компилятор так, чтобы он делал вещи в следующем порядке:

  • постоянное свертывание
  • анализ потока
  • перезапись дерева выражений
  • арифметикаоптимизация
  • codegen

Что, очевидно, требует критического изменения.

Теперь я решил сохранить существующее нарушенное поведение, выполнив следующее:

  • постоянное свертывание
  • арифметическая оптимизация
  • анализ потока
  • арифметическая де-оптимизация
  • перезапись дерева выражений
  • повторная оптимизация арифметики
  • codegen

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

3 голосов
/ 24 января 2012

В спецификации указывается, что определенное назначение чего-либо, что назначено только внутри блока if, не определено. В спецификации ничего не говорится о магии компилятора, которая удаляет ненужный блок if. В частности, это приводит к очень запутанному сообщению об ошибке, когда вы изменяете условие if, и внезапно получаете ошибку о том, что y не назначено "да? Я не изменился, когда назначен y!".

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

В частности, раздел 5.3.3.5 (спецификация MS 4.0):

5.3.3.5 Если утверждения Для оператора if в виде:

if ( expr ) then-stmt else else-stmt

  • v имеет то же состояние определенного присваивания в начале выражения, что и в начале stmt.
  • Если v определенно назначен в конце выражения, тогда он определенно назначается при передаче потока управления в then-stmt и либо в else-stmt, либо в конечную точку stmt, если нет предложения else.
  • Если v имеет состояние «определенно назначено после истинного выражения» в конце выражения, тогда оно определенно присваивается при передаче потока управления в then-stmt и не определенно присваивается при передаче потока управления в else-stmt или до конечной точки stmt, если нет предложения else.
  • Если v имеет состояние «определенно назначенное после ложного выражения» в конце выражения, тогда оно определенно присваивается при передаче потока управления в else-stmt и не определенно присваивается при передаче потока управления в then-stmt. Он определенно назначается в конечной точке stmt тогда и только тогда, когда он определенно назначается в конечной точке then-stmt.
  • В противном случае v считается определенно не назначенным при передаче потока управления ни на then-stmt, ни else-stmt, ни на конечную точку stmt, если больше нет

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

технически , путь выполнения существует там, где условие if ложно; если y также было назначено в else, то хорошо, но ... спецификация явно не требует, чтобы условие if всегда выполнялось.

...