Подтвердить что ты не робот

Какова причина того, чтобы семантика a = a ++ была undefined?

a = a++;

- это undefined поведение в C. Вопрос, который я задаю, это: почему?

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

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

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

4b9b3361

Ответ 1

UPDATE: этот вопрос был тема моего блога 18 июня 2012 г.. Спасибо за отличный вопрос!


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

(**) "undefined" поведение означает, что код может делать что угодно, включая удаление жесткого диска. Компилятору не требуется генерировать код, который имеет какое-либо конкретное поведение, и не обязан сообщать вам, что он генерирует код с поведением undefined. "Определенное исполнение" означает, что автору компилятора предоставляется значительная свобода выбора стратегии реализации, но он должен выбирать стратегию, использовать ее последовательно и документировать этот выбор.

() При наблюдении из одного потока, конечно.

Ответ 2

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

Ответ 3

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

Ответ 4

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

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

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

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

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

Ответ 5

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

Ответ 6

Кто-то может предоставить другую причину, но из оптимизации (лучше сказать, презентация ассемблера) точка зрения a должна быть загружена в регистр CPU, значение оператора postfix должно быть помещено в другой регистр или тот же. Таким образом, последнее назначение может зависеть либо от оптимизатора, либо от одного регистра или двух.

Ответ 7

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

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

Ответ 8

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

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

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