В чем причина того, что семантика a = a ++ не определена? - PullRequest
32 голосов
/ 30 марта 2012
a = a++;

- неопределенное поведение на языке C. Я задаю вопрос: почему?

Я имею в виду, что может быть трудно обеспечить последовательный порядок, в которомвсе должно быть сделано.Но некоторые компиляторы всегда будут делать это в том или ином порядке (на заданном уровне оптимизации).Итак, почему именно это должен решать компилятор?

Чтобы было ясно, я хочу знать, было ли это дизайнерским решением, и если так, что вызвало это?Или, может быть, есть какое-то аппаратное ограничение?

(Примечание: если заголовок вопроса кажется неясным или недостаточно хорошим, тогда приветствуются отзывы и / или изменения)

Ответы [ 8 ]

46 голосов
/ 30 марта 2012

ОБНОВЛЕНИЕ: Этот вопрос был темой моего блога 18 июня 2012 года . Спасибо за отличный вопрос!


Почему? Я хочу знать, было ли это дизайнерским решением, и если да, что это вызвало?

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

Однако я могу ответить на более широкий вопрос:

Какие факторы побуждают комитет по разработке языков оставлять поведение легальной программы (*) "неопределенным" или "определенным реализацией" (**)?

Первый важный фактор: Существуют ли две существующие реализации языка на рынке, которые не согласны с поведением конкретной программы? Если компилятор FooCorp компилирует M(A(), B()) как "вызов A, вызовите B , вызовите M ", и компилятор BarCorp скомпилирует его как" вызовите B, вызовите A, вызовите M ", и ни одно из них не будет" явно правильным "поведением, тогда у комитета по языковому проектированию есть сильный стимул сказать" вы оба правы " и сделайте так, чтобы реализация определяла поведение. В частности, это тот случай, когда FooCorp и BarCorp имеют представителей в комитете.

Следующий важный фактор: предоставляет ли эта функция много разных возможностей для реализации? Например, в C # анализ компилятора выражения "понимание запроса" определен как "выполнить синтаксическое преобразование в эквивалентная программа, которая не имеет понимания запросов, а затем нормально анализирует эту программу ". У реализации очень мало свободы, чтобы делать иначе.

В отличие от этого, спецификация C # гласит, что цикл foreach должен рассматриваться как эквивалентный цикл while внутри блока try, но обеспечивает реализацию некоторую гибкость. Компилятору C # разрешается говорить, например: «Я знаю, как реализовать семантику цикла foreach более эффективно для массива» и использовать функцию индексации массива, а не преобразовывать массив в последовательность, как того требует спецификация.

Третий фактор: настолько сложна функция, что подробное описание ее точного поведения будет сложно или дорого определить? Спецификация C # действительно очень мало говорит о том, как анонимные методы, лямбда-выражения, должны быть реализованы деревья выражений, динамические вызовы, блоки итераторов и асинхронные блоки; он просто описывает желаемую семантику и некоторые ограничения на поведение и оставляет остальное до реализации.

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

Func<int, int> f1 = (int x)=>x + 1;
Func<int, int> f2 = (int x)=>x + 1;
bool b = object.ReferenceEquals(f1, f2);

Предположим, мы требуем, чтобы b было правдой. Как вы собираетесь определить, когда две функции "одинаковы" ? Проводя анализ «интенсиональности» - тела функций имеют одинаковое содержание? - сложно и выполняется анализ «экстенсиональности» - дают ли функции одинаковые результаты при одинаковых входных данных? - еще сложнее. Комитет по спецификации языка должен стремиться свести к минимуму количество открытых исследовательских задач, которые должна решить группа по внедрению!

В C # это поэтому остается определяемым реализацией; по своему усмотрению компилятор может сделать их ссылками равными или нет.

Пятый фактор: накладывает ли функция высокую нагрузку на среду выполнения?

Например, в C # разыменование за концом массива четко определено; это производит исключение индекса массива, вышедшего за пределы. Эта функция может быть реализована с небольшой - не нулевой, а небольшой - стоимостью во время выполнения. Вызов экземпляра или виртуального метода с нулевым получателем определяется как создание исключения null-was-dereferenced; опять же, это может быть реализовано с небольшой, но ненулевой стоимостью. Преимущество устранения неопределенного поведения окупается небольшими затратами времени выполнения.

Шестой фактор: исключает ли определенное поведение некоторую серьезную оптимизацию ? Например, C # определяет порядок побочных эффектов при наблюдении из потока, который вызывает побочные эффекты . Но поведение программы, которая наблюдает побочные эффекты одного потока от другого потока, определяется реализацией, за исключением нескольких «специальных» побочных эффектов. (Подобно изменчивой записи или вводу блокировки.) Если язык C # требует, чтобы все потоки наблюдали одинаковые побочные эффекты в одном и том же порядке, то мы должны были бы ограничить современные процессоры от эффективного выполнения своей работы; современные процессоры зависят от исполнения вне очереди и сложных стратегий кэширования для достижения высокого уровня производительности.

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

Теперь вернемся к вашему конкретному примеру.

Язык C # делает строго определенным для этого поведения (); наблюдается побочный эффект приращения до появления побочного эффекта от назначения. Поэтому здесь не может быть никакого аргумента «ну, это просто невозможно», потому что можно выбрать поведение и придерживаться его. Это также не исключает больших возможностей для оптимизации. И не существует множества возможных сложных стратегий реализации.

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


(*) Или, иногда, его компилятор! Но давайте проигнорируем этот фактор.

(**) Поведение «Undefined» означает, что код может делать что угодно , включая стирание вашего жесткого диска. Компилятор не обязан генерировать код, который имеет какое-либо конкретное поведение, и не обязан сообщать вам, что он генерирует код с неопределенным поведением. Поведение «определено реализацией» означает, что автору компилятора предоставляется значительная свобода в выборе стратегии реализации, но требуется выбрать стратегию , использовать ее последовательно и документ, который выбор .

() Конечно, при наблюдении из одной нити.

11 голосов
/ 30 марта 2012

Это не определено, потому что для написания такого кода нет веских причин, и, не требуя какого-либо особого поведения для поддельного кода, компиляторы могут более агрессивно оптимизировать хорошо написанный код.Например, *p = i++ может быть оптимизирован таким образом, что это приводит к сбою, если p указывает на i, возможно, потому, что два ядра записывают в одну и ту же ячейку памяти одновременно.Факт, что это также оказывается неопределенным в конкретном случае, когда *p явно записывается как i, чтобы получить i = i++, логически следует.

6 голосов
/ 30 марта 2012

Это неоднозначно, но не синтаксически неправильно. Каким должен быть a? И =, и ++ имеют одинаковое "время". Таким образом, вместо определения произвольного порядка он был оставлен неопределенным, поскольку любой порядок будет конфликтовать с одним из двух определений операторов.

5 голосов
/ 30 марта 2012

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

К сожалению, это означает, что результат выражения типа a = a++ будет варьироваться в зависимости от компилятора, настроек компилятора, окружающего кода и т. Д. Поведение определенно называется неопределенным в стандарте языка, так что разработчики компилятора неНе нужно беспокоиться об обнаружении таких случаев и постановке диагноза против них.Случаи типа a = a++ очевидны, но что-то вроде

void foo(int *a, int *b)
{
  *a = (*b)++;
}

Если это единственная функция в файле (или если ее вызывающий объект находится в другом файле), во время компиляции нет способа узнать* указывают ли a и b на один и тот же объект;чем ты занимаешься?

Обратите внимание, что вполне возможно предписать, чтобы все выражения оценивались в определенном порядке, и чтобы все побочные эффекты применялись в конкретный момент оценки;это то, что делают Java и C #, и в этих языках выражения типа a = a++ всегда четко определены.

3 голосов
/ 30 марта 2012

Кто-то может указать другую причину, но с точки зрения оптимизации (лучше, скажем, представления на ассемблере) a необходимо загрузить в регистр ЦП, значение оператора постфикса следует поместить в другой регистр или в тот же. Поэтому последнее назначение может зависеть от того, использует ли оптимизатор один регистр или два.

3 голосов
/ 30 марта 2012

Оператор постфикса ++ возвращает значение до приращения. Итак, на первом шаге a присваивается его старое значение (вот что возвращает ++). На следующем этапе не определено, будет ли сначала выполняться приращение или присвоение, поскольку обе операции применяются к одному и тому же объекту (a), а язык ничего не говорит о порядке вычисления этих операторов.

2 голосов
/ 30 марта 2012

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

  • потому что это делает авторов компиляторов счастливее
  • потому что это позволяет реализациям определять его в любом случае
  • , потому что оно не навязывает определенное ограничение, когда оно не нужно
  • ...
1 голос
/ 10 августа 2012

Предположим, a - указатель со значением 0x0001ffff.И предположим, что архитектура сегментирована так, что компилятор должен применять приращение к верхним и нижним частям отдельно, с переносом между ними.Оптимизатор мог бы переупорядочить записи так, чтобы окончательное сохраненное значение было 0x0002ffff;то есть нижняя часть перед приращением и верхняя часть после приращения.

Это значение вдвое больше, чем вы могли ожидать.Он может указывать на память, не принадлежащую приложению, или может (в общем случае) быть представлением перехвата.Другими словами, процессор может вызвать аппаратную ошибку, как только это значение будет загружено в регистр, что приведет к сбою приложения.Даже если это не вызывает немедленного сбоя, использование приложения является совершенно неправильным значением.

То же самое может случиться с другими базовыми типами, а язык C позволяет даже целыместь захватывающие представления.C пытается обеспечить эффективную реализацию на широком спектре оборудования.Получить эффективный код на сегментированной машине, такой как 8086, сложно.Благодаря такому неопределенному поведению у разработчика языка есть немного больше возможностей для агрессивной оптимизации.Я не знаю, имел ли он когда-либо разницу в производительности на практике, но, очевидно, языковой комитет хотел дать оптимизатору все преимущества.

...