Порядок вычисления индексов массива (по сравнению с выражением) в C - PullRequest
47 голосов
/ 13 января 2020

Глядя на этот код:

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 какая-либо деталь, указывающая приоритет работы в данном конкретном случае?

Ответы [ 5 ]

51 голосов
/ 13 января 2020

Порядок левого и правого операндов

Чтобы выполнить присваивание в 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, в котором говорится:

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

Это довольно знакомо многим C практикам что поведение таких выражений, как x + x++, не определено стандартом C, поскольку они оба используют значение * 1 034 * и отдельно изменить его в том же выражении без последовательности. Однако в этом случае у нас есть вызов функции, который обеспечивает некоторую последовательность. global_var используется в arr[global_var] и обновляется при вызове функции update_three(2).

6.5.2.2 10 сообщает нам, что перед вызовом функции есть точка последовательности:

Существует точка последовательности после вычислений обозначения функции и фактических аргументов, но перед фактическим вызовом…

Внутри функции global_var = val; является полным выражением и так же 3 in return 3;, согласно 6.8 4:

A полное выражение является выражением, которое не является частью другого выражения или частью декларатор или абстрактный декларатор…

Тогда между этими двумя выражениями есть точка последовательности, опять же в соответствии с 6.8 4:

… Между точкой вычисления есть точка последовательности полное выражение и вычисление следующего полного выражения для оценки.

Таким образом, реализация C может сначала оценить arr[global_var], а затем выполнить вызов функции, и в этом случае есть последовательность точка между т hem, потому что перед вызовом функции стоит один, или он может оценить global_var = val; в вызове функции, а затем arr[global_var], и в этом случае между ними будет точка последовательности, потому что после полного выражения есть одна. Таким образом, поведение не определено - любая из этих двух вещей может быть оценена первой - но оно не является неопределенным.

24 голосов
/ 13 января 2020

Результат здесь неопределен .

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

Существует , а не неопределенное поведение. здесь, потому что вызов функции вводит точку последовательности, как и каждый оператор в функции, включая оператор, который модифицирует global_var.

Для пояснения, C стандарт определяет неопределенное поведение в разделе 3.4.3 как:

неопределенное поведение

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

и определяет неопределенное поведение в разделе 3.4.4 как:

неопределенное поведение

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

Стандарт гласит, что порядок вычисления аргументов функции не определен, что в данном случае означает, что либо arr[0] устанавливается в 3, либо arr[2] устанавливается в 3.

1 голос
/ 13 января 2020

Я попытался, и я получил обновленную запись 0.

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

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

0 голосов
/ 15 января 2020

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

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

разработали и внедрили несколько критически важных встраиваемых систем реального времени в дни K & R (включая контроллер электрического автомобиля c, который мог отправить людей, врезавшихся в ближайшую стену, если двигатель не контролировался, 10-тонный промышленный робот, который мог бы собрать 1039 * людей в целлюлозу, если бы ему не командовали должным образом, и системный уровень, который, хотя и был бы безвреден, имел бы несколько десятков процессоров, высасывающих их шину данных dry с нагрузкой на систему менее 1%).

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

Все это Нагрузка сглаживающих абстракций барьера памяти происходит просто из-за временного набора ограничений многопроцессорных систем кэширования, которые могут быть безопасно инкапсулированы в общие объекты синхронизации ОС, такие как, например, мьютексы и переменные состояния, которые предлагает C ++.
Стоимость такой инкапсуляции всего лишь незначительное снижение производительности по сравнению с тем, чего может достичь использование мелкозернистых специфических c инструкций ЦП в некоторых случаях.
Ключевое слово volatile (или #pragma dont-mess-with-that-variable для Мне, как системному программисту, все равно было бы достаточно, чтобы сказать компилятору прекратить переупорядочивать доступ к памяти. Оптимальный код может быть легко получен с помощью прямых asm-директив для разбрызгивания низкоуровневого драйвера и кода ОС с помощью специальных инструкций c CPU c. Без глубоких знаний о том, как работает базовое оборудование (кеш-система или интерфейс шины), вы все равно будете писать бесполезный, неэффективный или неисправный код.

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

Только на этот раз изменения потребовались также для искажения фундаментального аспекта C, поскольку эти «барьеры» должны были генерироваться даже в низкоуровневом коде C для правильной работы. Это, помимо всего прочего, вызвало c в определении выражений, без каких-либо объяснений или оправданий.

В заключение, тот факт, что компилятор может производить согласованный машинный код из этого абсурдного фрагмента C является лишь отдаленным следствием того, как ребята из C ++ справлялись с потенциальными несоответствиями систем кэширования в конце 2000-х годов.
Это ужасно запутало один фундаментальный аспект C (определение выражения), так что Подавляющее большинство C программистов - которым наплевать на системы кеширования, и это правильно - теперь вынуждены полагаться на гуру, чтобы объяснить разницу между a = b() + c() и a = b + c.

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

Я также не могу понять важность этой неопределенной / неопределенной дискуссии. Либо вы можете положиться на компилятор для генерации кода с единообразным поведением, либо вы не можете. Называете ли вы это неопределенным или неопределенным, кажется спорным.

По моему, возможно, обоснованному мнению, C уже достаточно опасен в своем состоянии K & R. Полезной эволюцией будет добавление мер безопасности здравого смысла. Например, используя этот усовершенствованный инструмент анализа кода, спецификации заставляют компилятор, по крайней мере, генерировать предупреждения о неумелом коде, вместо того, чтобы молча генерировать код, потенциально ненадежный до крайности.
Но вместо этого ребята решили, например, , чтобы определить фиксированный порядок оценки в C ++ 17. Теперь каждый программный дурак активно настроен на преднамеренное использование побочных эффектов в его / ее коде, греясь в уверенности, что новые компиляторы будут охотно обрабатывать запутывание определенным образом c.

K & R был одним из истинные чудеса компьютерного мира. За двадцать баксов вы получили исчерпывающую спецификацию языка (я видел, как отдельные люди пишут полные компиляторы, используя только эту книгу), отличное справочное руководство (оглавление обычно указывает вам на пару страниц ответа на ваш вопрос). вопрос), и учебник, который научит вас разумно использовать язык. В комплекте с обоснованиями, примерами и мудрыми словами предупреждения о многочисленных способах злоупотребления языком для совершения очень, очень глупых поступков.

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

0 голосов
/ 13 января 2020

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

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

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

int idx = global_var;
arr[idx] = update_three(2);

или кода:

int temp = update_three(2);
arr[global_var] = temp;

Хорошее правило: если глобальные переменные const (или нет, но вы знаете, что никакой код никогда не изменит их как побочный эффект), вы никогда не должны использовать их непосредственно в коде, как в многопоточной среде, даже это может быть неопределенным:

int result = global_var + (2 * global_var);
// Is not guaranteed to be equal to `3 * global_var`!

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

( Просто на тот случай, если кто-то спросит, зачем кому-то делать x + 2 * x вместо 3 * x - на некоторых процессорах добавление происходит очень быстро, так же как и умножение на степень два, так как компилятор превратит их в битовые сдвиги (2 * x == x << 1), но пока умножение на произвольные числа может быть очень медленным, поэтому вместо умножения на 3 вы получите намного более быстрый код, сдвинув бит на 1 и добавив х к результату - и даже этот трюк выполняется современными компиляторами, если умножить на 3 и повернуть на агрессивную оптимизацию, если только это не современный целевой процессор, где умножение одинаково быстро, как сложение с тех пор, как трюк замедлит вычисления.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...