Глядя на этот код:
static int global_var = 0;
int update_three(int val)
{
global_var = val;
return 3;
}
int main()
{
int arr[5];
arr[global_var] = update_three(2);
}
Какая запись массива обновляется? 0 или 2?
Есть ли часть в спецификации C, которая указывает приоритет работы в этом конкретном случае?
c
language-lawyer
order-of-execution
Jiminion
источник
источник
clang
чтобы этот фрагмент кода вызвал предупреждение IMHO.Ответы:
Порядок левого и правого операндов
Чтобы выполнить присваивание
arr[global_var] = update_three(2)
, реализация C должна оценить операнды и, в качестве побочного эффекта, обновить сохраненное значение левого операнда. C 2018 6.5.16 (что касается присвоений), параграф 3 говорит нам, что нет последовательности в левом и правом операндах:Это означает, что реализация C может сначала вычислить lvalue
arr[global_var]
(вычисляя lvalue, мы имеем в виду выяснение того, к чему относится это выражение), затем вычислитьupdate_three(2)
и, наконец, присвоить значение последнего первому; илиupdate_three(2)
сначала вычислить, затем вычислить l-значение, затем присвоить первое последнему; или для оценки lvalue иupdate_three(2)
каким-то смешанным образом, а затем присвоить правильное значение левому lvalue.Во всех случаях присвоение значения lvalue должно выполняться последним, потому что 6.5.16 3 также говорит:
Нарушение последовательности
Некоторые могут задуматься о неопределенном поведении из-за его использования
global_var
и отдельного обновления в нарушение 6.5 2, которое гласит:Многим специалистам по Си довольно хорошо известно, что поведение выражений, таких как
x + x++
, не определяется стандартом Си, поскольку они оба используют значениеx
и по отдельности изменяют его в одном и том же выражении без последовательности. Однако в этом случае у нас есть вызов функции, который обеспечивает некоторую последовательность.global_var
используетсяarr[global_var]
и обновляется в вызове функцииupdate_three(2)
.6.5.2.2 10 сообщает нам, что перед вызовом функции есть точка последовательности:
Внутри функции,
global_var = val;
является полное выражение , и так это3
вreturn 3;
, за 6,8 4:Тогда есть точка последовательности между этими двумя выражениями, снова в 6.8 4:
Таким образом, реализация C может
arr[global_var]
сначала выполнить оценку, а затем выполнить вызов функции, и в этом случае между ними есть точка последовательности, потому что перед вызовом функции существует единица, или она может выполнить оценкуglobal_var = val;
в вызове функции, а затемarr[global_var]
, в этом случае точка последовательности между ними, потому что есть одна после полного выражения. Таким образом, поведение не определено - любая из этих двух вещей может быть оценена первой - но оно не является неопределенным.источник
Результат здесь не уточняется .
Хотя порядок операций в выражении, который определяет, как подвыражения группируются, хорошо определен, порядок вычисления не указан. В этом случае это означает, что либо
global_var
может быть прочитано первым, либо вызовupdate_three
может произойти первым, но нет способа узнать, какой именно.Здесь нет неопределенного поведения, потому что вызов функции вводит точку последовательности, как и каждый оператор в функции, включая тот, который изменяет
global_var
.Для пояснения стандарт C определяет неопределенное поведение в разделе 3.4.3 как:
и определяет неопределенное поведение в разделе 3.4.4 как:
Стандарт гласит, что порядок вычисления аргументов функции не определен, что в данном случае означает, что либо
arr[0]
устанавливается в 3, либоarr[2]
в 3.источник
Я попытался, и я получил запись 0 обновлена.
Однако в соответствии с этим вопросом: всегда будет вычисляться правая часть выражения в первую очередь
Порядок оценки не определен и не упорядочен. Поэтому я думаю, что такого кода следует избегать.
источник
Так как бессмысленно выдавать код для назначения до того, как у вас есть значение для назначения, большинство компиляторов C сначала генерируют код, который вызывает функцию, и сохраняют результат где-нибудь (регистр, стек и т. Д.), Затем они генерируют код, который записывает это значение в конечный пункт назначения, и поэтому они будут читать глобальную переменную после ее изменения. Давайте назовем это «естественным порядком», определяемым не каким-либо стандартом, а чистой логикой.
Тем не менее, в процессе оптимизации компиляторы попытаются исключить промежуточный этап временного хранения значения где-либо и попытаются записать результат функции как можно более напрямую в конечный пункт назначения, и в этом случае им часто придется сначала прочитать индекс например, в регистр, чтобы иметь возможность напрямую перемещать результат функции в массив. Это может привести к тому, что глобальная переменная будет прочитана до ее изменения.
Так что это в основном неопределенное поведение с очень плохим свойством, поэтому вполне вероятно, что результат будет другим, в зависимости от того, была ли выполнена оптимизация и насколько агрессивна эта оптимизация. Ваша задача как разработчика решить эту проблему с помощью любой кодировки:
или кодирование:
Как хорошее правило: если глобальные переменные не являются
const
(или нет, но вы знаете, что ни один код не изменит их как побочный эффект), вы никогда не должны использовать их непосредственно в коде, как в многопоточной среде, даже это может быть неопределенным:Поскольку компилятор может прочитать его дважды, а другой поток может изменить значение между двумя чтениями. Тем не менее, опять же, оптимизация определенно приведет к тому, что код будет читать его только один раз, так что вы можете снова получить другие результаты, которые теперь также зависят от синхронизации другого потока. Таким образом, у вас будет намного меньше головной боли, если вы будете хранить глобальные переменные во временной переменной стека перед использованием. Имейте в виду, что если компилятор считает, что это безопасно, он, скорее всего, оптимизирует даже это, и вместо этого будет напрямую использовать глобальную переменную, поэтому, в конце концов, это может не повлиять на производительность или использование памяти.
(Просто на тот случай, если кто-то спросит, зачем кому-то делать
x + 2 * x
вместо этого3 * x
- на некоторых процессорах добавление происходит очень быстро и умножается на степень два, так как компилятор превратит их в битовые сдвиги (2 * x == x << 1
), но умножение на произвольные числа может быть очень медленным таким образом, вместо умножения на 3, вы получаете намного более быстрый код, сдвигая бит на 1 и добавляя х к результату - и даже этот трюк выполняется современными компиляторами, если вы умножаете на 3 и включаете агрессивную оптимизацию, если это не современная цель Процессор, в котором умножение одинаково быстр как сложение с тех пор, как трюк замедлит вычисления.)источник
3 * x
в два чтения x. Он может прочитать x один раз, а затем выполнить метод x + 2 * x в регистре, в который он читает xlanguage-lawyer
, где у рассматриваемого языка есть свое собственное "очень специальное значение" для неопределенного , вы будете только приводить в замешательство, не используя определение языка.Глобальное редактирование: извините, ребята, я разозлился и написал много глупостей. Просто старый чудак.
Я хотел верить, что C пощадили, но, увы, начиная с C11, он был приведен в соответствие с C ++. Очевидно, что знание того, что компилятор будет делать с побочными эффектами в выражениях, теперь требует разгадать небольшую математическую загадку, включающую частичное упорядочение последовательностей кода на основе «находится перед точкой синхронизации».
Мне довелось спроектировать и внедрить несколько критически важных встроенных систем реального времени еще в дни K & R (включая контроллер электромобиля, который мог отправить людей врезаться в ближайшую стену, если двигатель не контролировался, 10-тонный промышленный робота, который мог бы раздавить людей до полусмерти, если им не командовали должным образом, и системного уровня, который, хотя и был бы безвреден, имел бы несколько десятков процессоров, высасывающих их шину данных без нагрузки с нагрузкой системы менее 1%).
Возможно, я слишком стар или глуп, чтобы понять разницу между неопределенным и неопределенным, но я думаю, что у меня все еще есть достаточно хорошее представление о том, что означает параллельное выполнение и доступ к данным. По моему, возможно, осознанному мнению, эта одержимость C ++, а теперь и парней C с их любимыми языками, которые берут на себя вопросы синхронизации, является дорогой несбыточной мечтой. Либо вы знаете, что такое одновременное выполнение, и вам не нужны эти штуковины, либо нет, и вы сделаете весь мир одолжением, не пытаясь с ним связываться.
Вся эта куча отвратительных абстракций барьера памяти происходит просто из-за временного набора ограничений многопроцессорных систем кэширования, которые можно безопасно инкапсулировать в общие объекты синхронизации ОС, такие как, например, мьютексы и переменные состояния C ++. предложения.
Стоимость такой инкапсуляции всего лишь незначительное снижение производительности по сравнению с тем, чего в некоторых случаях может достичь использование мелкозернистых специфических инструкций процессора. Ключевое слово (или
volatile
#pragma dont-mess-with-that-variable
для меня, как системного программиста, было бы достаточно заботы, чтобы сказать компилятору прекратить переупорядочивать доступ к памяти. Оптимальный код может быть легко получен с помощью прямых asm-директив для посыпания низкоуровневого драйвера и кода ОС специальными инструкциями для процессора. Без глубоких знаний о том, как работает базовое оборудование (кеш-система или интерфейс шины), вы все равно будете писать бесполезный, неэффективный или неисправный код.Минутная корректировка
volatile
ключевого слова, и Боб был бы всем, кроме самого крутого дядюшки программистов низкого уровня. Вместо этого у обычной банды математиков в С ++ был полевой день, который разрабатывал еще одну непостижимую абстракцию, уступая их типичной тенденции разрабатывать решения, ища несуществующие проблемы и ошибочно определяя определение языка программирования со спецификациями компилятора.Только на этот раз изменение потребовало также ослабить фундаментальный аспект C, поскольку эти «барьеры» должны были генерироваться даже в низкоуровневом коде C, чтобы работать должным образом. Это, помимо всего прочего, нанесло ущерб определению выражений без каких-либо объяснений или оправданий.
В заключение следует отметить, что тот факт, что компилятор может генерировать согласованный машинный код из этого абсурдного фрагмента C, является лишь отдаленным следствием того, как ребята из C ++ справлялись с потенциальными несоответствиями систем кэширования в конце 2000-х годов.
Это привело к ужасной путанице в одном фундаментальном аспекте C (определение выражений), так что подавляющее большинство программистов на C - которым наплевать на системы кэширования, и это правильно - теперь вынуждены полагаться на гуру для объяснения Разница между
a = b() + c()
иa = b + c
.Попытка угадать, что будет с этим неудачным массивом, в любом случае будет чистой потерей времени и усилий. Независимо от того, что компилятор сделает из этого, этот код патологически неверен. Единственная ответственная вещь, которую нужно сделать, это отправить ее в мусорное ведро.
Концептуально, побочные эффекты всегда могут быть исключены из выражений, с тривиальной попыткой явно разрешить изменение до или после оценки в отдельном утверждении.
Этот дерьмовый код мог быть оправдан в 80-х годах, когда нельзя было ожидать, что компилятор что-то оптимизирует. Но теперь, когда компиляторы давно стали более умными, чем большинство программистов, все, что остается, - это кусок дерьмового кода.
Я также не понимаю важность этой неопределенной / неопределенной дискуссии. Либо вы можете положиться на компилятор для генерации кода с единообразным поведением, либо вы не можете. Называете ли вы это неопределенным или неопределенным, кажется спорным вопросом.
По моему, возможно, обоснованному мнению, C уже достаточно опасен в своем состоянии K & R. Полезной эволюцией будет добавление мер безопасности на основе здравого смысла. Например, используя этот усовершенствованный инструмент анализа кода, спецификации заставляют компилятор, по крайней мере, генерировать предупреждения о беккерах, вместо того, чтобы молча генерировать код, потенциально ненадежный до крайности.
Но вместо этого ребята решили, например, определить фиксированный порядок оценки в C ++ 17. Теперь каждый программный дурак активно настроен на то, чтобы целенаправленно добавлять побочные эффекты в свой код, греясь в уверенности, что новые компиляторы будут охотно обрабатывать обфускацию детерминистическим способом.
K & R была одним из настоящих чудес компьютерного мира. За двадцать баксов вы получили исчерпывающую спецификацию языка (я видел, как отдельные люди пишут полные компиляторы, просто используя эту книгу), отличное справочное руководство (оглавление обычно указывает вам на пару страниц ответа на ваш вопрос). вопрос), и учебник, который научит вас разумно использовать язык. В комплекте с обоснованиями, примерами и мудрыми словами предупреждения о многочисленных способах злоупотребления языком, чтобы делать очень, очень глупые вещи.
Уничтожение этого наследия ради такой маленькой выгоды кажется мне жестокой тратой. Но, опять же, я вполне могу не понять суть полностью. Может быть, какая-то добрая душа могла бы указать мне на пример нового кода на С, который бы получил значительное преимущество от этих побочных эффектов?
источник
0,expr,0
.