Как стать автором
Обновить

Комментарии 72

Я бы уточнил, что, например, переменная countPeople может быть изменена не только программой, а и контроллером лифта. Это и будет то самое "извне", которое должен уважать компилятор.

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

НЛО прилетело и опубликовало эту надпись здесь

// Значение переменной countPeople к примеру будет менять с другого потока

Но ведь изменения из другого потока это внутренние изменения и компилятор о них прекрасно знает, ведь код другого потока он тоже компилирует. Тут скорее идет речь о написании драйвера.

Представьте пишите вы код для микроконтроллера но без ядра поэтому функции типа digitalWrite вы не можете использовать и тогда вы идете в даташит и находите по какому смещению в памяти находится массив переменных gpio. После этого вы производите выделение нужной вам памяти и объявляете эту память volatile .

По стандарту – это не его дело.

И вообще, анализировать весь ваш код (часть из которого может быть вообще в другом модуле, который компилируется отдельно) – неблагодарная задача. Лучше потребовать от вас использовать примитивы синхронизации (как уже отметили, не volatile).

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

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

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

Насчёт const – да. Модификатор "const volatile" особенно прекрасен (при том, что это обычное дело на мк).

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

Т.е., допустим, у нас есть глобальная переменная v. Пусть функция foo присваивает ей какое-то значение (скажем, константу) и больше не трогает. С этого момента компилятор может считать, что в v это значение. Он может положить его туда сразу, может не класть, а просто при всех использованиях v подставлять эту константу. Даже если функция foo вызывает функцию bar, лежащую в том же модуле.

Ситуация меняется в трёх случаях:

  1. Мы выходим из функции foo. Поскольку при этом мы можем попасть в другой модуль (другую единицу трансляции) – придётся к этому моменту записать значение в память, вдруг кому-то понадобится.

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

  3. Примитивы синхронизации (mutex, к примеру). Пока мы ждём mutex – все наши переменные могут изменить снаружи. Значит, их на это время надо положить в память, а потом доставать по мере необходимости.

    Нетрудно убедиться, что 1-2 решают упомянутые вами проблемы однопоточного кода. А 3 при правильном использовании – и многопоточного.

    Извините за такое упрощённое описание, я упирал на понятность, а не точность. Формальные детали лучше бы читать в стандарте.

Но ведь изменения из другого потока это внутренние изменения и компилятор о них прекрасно знает

Абсолютно не так. Другой поток может быть в другой библиотеке или, что чаще, другом объектнике и скомпиллирован другим инстансом.

Почитайте про Еденицу трансляции

Вот пример записи в порт атмеги:

volatile uint8_t * portd = 0x2b;
 *portd = 0xFF;

И картинка:

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

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

Поэтому ключевое слово volatile для таких случаев не требуется.

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

Никогда, слышите, никогда не используйте volatile в одном предложении с multithreading. Единственное исключение: предыдущее предложение.

https://stackoverflow.com/questions/4557979/when-to-use-volatile-with-multi-threading

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

Пример использования volatile:

volatile uint16_t* reg = 0x1234567;
*reg = 1;
while (*reg == 1) {
  // do something
}

Без volatile компилятор просто заменит условие на `while (true)`, потому что значение было присвоено в 1, и проверяется 1. При объявлении переменной как volatile компилятор не будет делать никаких оптимизаций при обращении к переменной, и всегда будет честно читать ее значение.
Как уже отмечалось в комментариях, это используется в программировании контроллеров/..., когда значение по адресу может меняться внешним образом.

это вы компилятор.

а есть еще и процессор - и у него есть свои оптимизации и интересные спецэффекты

(изза которых люди открывают для себя атомарные операции и барьеры памяти :) )

Делаю библиотечку в которой будет функция routine, она будет крутиться в отдельном thread в моём основном приложении. В этой функции есть флажок изменяемый только из приложения, я вот не уверен что без volatile флажок не будет соптимизирован => volatile флажок?

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

Где-нибудь на 8-битных м/контроллерах это приемлемо для общения с прерываниями, например (в сторону повышения приоритета), но на чем-то сложнее лучше использовать как минимум rtos и встроенные механизмы ОС - всякие семафоры, shared memory и т.п.. чтобы не изобретать велосипед.

Не надо так. Вы ж под этим флажком наверняка будете менять какие-то данные (ну или сигнализировать флажком об их изменении) – а на них этот volatile не распространяется.

Используйте семафор.

Очевидно что флажок обрамляется примитивом синхронизации. Я просто не уверен что он сам может быть не volatile. Хотя посидел на godbolt и поковырял классического производителя-потребителя, без volatile криминала не увидел.

Если обрамляется – volatile не нужен. Мы ж под этими примитивами не то что флажки – сложные структуры данных "отдаём" в другой тред.

С чего бы? Как вы хотите иначе делать атомарные операции на тех же счётчиках ссылок например?

Через православные std::atomic<>?

никогда не используйте volatile в одном предложении с multithreading

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

При наличии прямых рук нет ничего лучше, чем хорошо спроектированный lockless с соответствующими volatile-ами и барьерами - тот же DPDK юзает в хвост и гриву все вышеуказанное

Для C++-ных junior-ов да, лучше использовать высокоуровневые абстракции типа std atomic-ов, но в целом для highload многопоточных приложений без volatile, барьеров, фенов, CAS-ов - никуда

Казалось бы, а причем тут C, на котором написан DPDK, и его работа с регистрами?

Разве ж C и C++ отличаются в поведении относительно volatile?

Проверенная годами и highload-нагрузкой схема - volatile, CAS, выравнивание по линейке кеша, серийник в старших битах для обхода ABA-проблемы - и примитив для многопоточного lockless-алгоритма на X86-64 архитектуру, даже с множеством numa-нод готов - в большинстве случаев даже fence-ы не нужны

Понятно что есть красивый фасад в виде std::atomic вокруг кучи intrinsic-ов, и джунам лучше использовать его, чтобы не выстрелить себе в ногу, но в целом же это банальный синтаксический сахар, и странно говорить что volatile не подходит для многопоточности, в то время как он отлично подходит в решениях, проверенных годами

В конце концов, интринсик всего лишь или вставляет машинную инструкцию, или же инструктирует компилятор об аспектах кодогенерации (например запрещает переставлять операции в целях оптимизации, или маркирует неявную возможную замену объекта при девиртуализации в std::launder, к примеру)

Это примерно из того же разряда, как проверять свойства типов в compile time - классическим SFINAE или через темплейтную auto-лямбду? У второго подхода есть свои плюсы, но странно говорить, что старый добрый SFINAE уже не подходит для этого

На volatile вы запишете значения в переменные в порядке a=1; b=2;, а соседний поток может увидеть сначала b, а потом а.

Так где выше хоть слово о том, что так `a=1; b=2;` надо делать - там же явно написано

Проверенная годами и highload-нагрузкой схема - volatile, CAS, выравнивание по линейке кеша, серийник в старших битах для обхода ABA-проблемы

При соблюдении всех требований целевой архитектуры - вполне себе рабочее решение :)

Я не совсем понимаю, что вы хотите доказать. В программировании в 99,(9)% случаев использование volatile в контексте многопоточности - это ошибка и непонимание, как оно работает.

Очевидно есть специальные случаи, когда это все нужно.

Я особенно ничего доказывать не хочу, мне просто слишком резанула глаз цитата про единственное исключение - все-таки исключение-то не единственное, и вполне себе можно сочетать volatile и multithreading при должно сноровке :)

Никогда, слышите, никогда не используйте volatile в одном предложении с multithreading. Единственное исключение: предыдущее предложение.


А так спору нет, более того, вне экзотических случаев, лучше вообще использовать готовенький TBB, решающий большинство проблем подо все адекватные платформы :)

Из драфта стандарта C++11, 1.10 Multi-threaded executions and data races [intro.multithread], абзац 21:

The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior.

Как видите, никаких поблажек volatile-у не сделано.

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

atomic-и (особенно с явным указанием memory_order, особенно если это memory_order_relaxed) --- ни разу не "высокоуровневая абстракция".

Все-таки volatile + __sync_* intrinsics дают большие возможности контроля Compiler Explorer (godbolt.org)

Atomic конечно соберется с любым процессором, но может оказаться, что там всадили lock - в каких-то случаях это плюс, но в каких-то лучше пусть не скомпилируется, зато это повод явно разработать lockless-версию для целевой платформы, и не нарваться на lock

Хотя вы конечно скажите `static_assert(std::atomic<T>::is_always_lock_free == true, "LOCK DETECTED ALARM")` - в общем, на вкус, на цвет, и на грануларность контроля :)

А при чём здесь volatile? В описании __sync_val_compare_and_swap про него ничего не сказано -- ни в доках gcc, ни в доках Intel Itanium Processor-specific Application Binary Interface, откуда он родом. И если убрать в вашем коде volatile, ассемблерный код не изменится. В том числе при сборке с оптимизацией (-O2).

В каком-нибудь таком случае вполне себе изменит https://godbolt.org/z/brn7brjoE , плюс все __sync_* функции все равно форсированно приводят указатель к volatile, благодаря чему оно верно и компилируется. Условный TBB от Intel тоже вполне себе использует volatile в многопоточном коде Search · volatile (github.com)

В общем, не вижу смысла в дальнейшей дискуссии. Исходный тезис был "multithreading и многопоточность несовместимы", а это не верно. Показать примеры из промышленного кода я не могу по NDA, а сидеть выдумывать аналогичные простые примеры - не вижу смысла

Чтобы подвести bottom line - смысл volatile (не технически, а скорее философски) примерно такой же, как у std::launder - надавать компилятору по рукам, где в высокооптимизированном (и скорее всего массивно-многопоточном) highload-коде проведены ручные микрооптимизации под целевую платформу, и надо отвадить компилятор от неверной кодогенерации :)

про цикл как то не очевидно по моему, одна инструкция аля decfsz для счетчика и погнали, небось как всегда за один такт?

Проверка условия плоха ветвлением. Без него вся эта пачка присвоений тоже будет за 1 такт.

а с угаданными переходами тоже за 1?

volatile это вообще зло непонятно как пропущенное в стандарт. В С# то-же самое.
Как им правильно пользоваться, никто не знает, но на всякий случай "если меняете переменную в трэдах пишите volatile", типа не ошибетесь. А вот фиг там. Если уж приспичило написать странный код, меняющий глобальные переменные, то пишите хотя бы на mutex эту логику. Хотя применений, изменению глобальных переменных, довольно много. Например запись лог-файла из нескольких несинхронизированных потоков. Да много чего еще.

В C/C++ аналог volatile из C# это atomic всёже.

я думаю, его нужно было назвать "modified_outside_of_code" чтобы точнее передать суть.

То, что его используют еще и для передачи каких-то значений в многопоточке - это ИМХО unintended use

volatile = изменчивый. Что тут непонятного? Кто-то сам себе придумал, что volatile магическим образом помогает в мультипоточных приложениях(или просто не разобрался в вопросе) и теперь обижается.

Да не проблема в "изменчивости" вовсе. Совершенно. Это как угодно можно реализовать. Просто volatile преподносится так, как будто это "золотая пуля" для переменных которым нужно меняться в разных потоках. На самом деле - фигушки. Это просто директива компилятору что оно будет меняться "атомарно". Т.е. не одновременно. Т.е. set через некий семафор\мьютекс или как угодно это назовите. Принцип тот-же. В одну единицу времени значение меняется одним потоком.

"атомарно" это как раз одновременно. Атомарность к volatile не имеет вообще никакого отношения.

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

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

Самая затратная операция в этом примере не присваивание ячейке массива какого-либо значения и не инкремент счетчика, а именно операция сравнения, поэтому компилятор оптимизирует это примерно вот так.

Операция сравнения не является самой затратной. Откройте страницу из Интел мануала на ваш процессор и посмотрите что занимает она один такт. Оптимизация, которую выполнил компилятор в данном случае называется loop unrolling и она нужна для того, чтобы увеличить количество полезной работы за одну итерацию цикла. Грубо говоря мы уменьшаем количество проверок на конец цикла, а значит повышаем быстродействие.

volatile нужен по нескольким очевидным причинам. И главная, это запретить компилятору убирать эту переменную в результате dead code ellimination и прочих подстановок с упрощением выражений и вытаскиваний инвариантов. volatile для межпоточного взаимодействия это очень плохой пример, потому как это должен быть как минимум atomic.

#Всего_один_такт_с_очисткой_конвейера_это_быстро

volatile это вообще зло непонятно как пропущенное в стандарт. В С# то-же самое. Как им правильно пользоваться, никто не знает, но на всякий случай "если меняете переменную в трэдах пишите volatile", типа не ошибетесь.

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

C#

On a multiprocessor system, a volatile read operation is not guaranteed to obtain the latest value written to that memory location by any processor. Similarly, a volatile write operation does not guarantee that the value written would be immediately visible to other processors.

On a uniprocessor system, volatile reads and writes ensure that a value is read or written to memory and not cached (for example, in a processor register). Thus, you can use these operations to synchronize access to a field that can be updated by another thread or by hardware.

  • For non-volatile fields, optimization techniques that reorder instructions can lead to unexpected and unpredictable results in multi-threaded programs that access fields without synchronization such as that provided by the lock_statement (§12.13). These optimizations can be performed by the compiler, by the run-time system, or by hardware.

volatile (C++)

A type qualifier that you can use to declare that an object can be modified in the program by the hardware.

You can use the volatile qualifier to provide access to memory locations that are used by asynchronous processes such as interrupt handlers.

This enables volatile objects to be used for memory locks and releases in multithreaded applications.

When it relies on the enhanced guarantee that's provided when the /volatile:ms compiler option is used, the code is non-portable.

Java

The volatile keyword does not cache the value of the variable and always read the variable from the main memory. The volatile keyword cannot be used with classes or methods. However, it is used with variables. It also guarantees visibility and ordering. It prevents the compiler from the reordering of code.

The contents of the particular device register could change at any time, so you need the volatile keyword to ensure that such accesses are not optimized away by the compiler.

Типовая проблема - типовое решение.

On a uniprocessor system

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

Потому что сейчас чтобы найти такое у массового пользователя мобилок\ноутов\десктопов - это уже надо сильно постараться (мне кажется уже скоро в ардуину будут пихать многопроц)

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

О, а можно чуть-чуть подробностей? (достаточно номера мк и пары слов для поиска в datasheet)

https://community.st.com/s/article/FAQ-DMA-is-not-working-on-STM32H7-devices

Смотрите "2. Explanation: handling DMA buffers with D-Cache enabled" и "5. Solution example 3: Use Cache maintenance functions".

Хотя мне кажется я встречался с этим не на H7, а на F7. Но это не точно.

Не знаю как в C# и C++, но в яве volatile это про барьер happens before, а вовсе не про кэши

const MAX_COUNT_PEOPLE = 4;

Каков тип этой постоянной?

Небось джаваскриптовые привычки

C позволяет опускать int во многих случаях. Например, если вы напишете


typedef T;
const A = 1;
main(argc, argv)
    char **argv;
{
    return A;
}

, то компилятор (gcc и clang) поругается на те места, где опущен int, но скомпилирует (на argc clang ругается только с -pedantic). Не знаю, правда, что из этого есть в стандарте, но, учитывая что при указании -std=c89 и gcc, и clang перестают ругаться (у clang надо ещё убрать -pedantic), я полагаю, что такой код был вполне допустим C89.


Также, из имеющихся у меня компиляторов есть ещё tcc, который глотает код без каких‐либо предупреждений, bcc, который отказывается компилировать и pcc, который компилирует данный код без предупреждений, только результат компиляции откуда‐то ловит SEGV.

C позволяет опускать int во многих случаях.

А C++?

Статья называется "Ключевое слово «volatile» C/C++".

Это -- один момент.

Второй момент связан с тем, что под if'ом идёт сравнение на меньше/больше знакового с беззнаковым, на что компиляторы как правило выдают предупреждение.

Причём, ещё и размеры этих знаковых беззнаковых могут отличаться...

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

const MAX_COUNT_PEOPLE = 4;
size_t countPeole = 0;
...
if(countPeople > MAX_COUNT_PEOPLE)
...

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

Я чего-то не понял, или все-таки условие всегда будет ложно?

компилятор оптимизирует это примерно вот так:

at[i] = ..;

ar[i + 1] = ...;

ar[i + 2] = ...;

ar[i + 3] = ...;

Проверял. Нет, GCC не настолько крут.

Самая затратная операция в этом примере не присваивание ячейке массива какого-либо значения и не инкремент счетчика, а именно операция сравнения

Чем же инструкция сравнения "дороже"любой другой операции?

Да, за один проход цикла обрабатывать 4 ячейки массива будет быстрее, чем одну, но это из-за микроархитектуры современных процов, а не отсутствия "лишних" сравнений.

пример, в котором имеем массив символов, с помощью цикла проходим по всей строке

for(size_t i = 0; i < strlen(str); i++)

{

...

}

В Си строка и массив байтового размера — несколько разные вещи. Для массива надо проходить именно по длине, а для циклов по символам строки достаточно:


while (*str) {...}

Правда, если у вас массив char не имеет последним символом '\0', то и strlen(char* str) работать не будет.

Чем же инструкция сравнения "дороже"любой другой операции?

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

https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/condition-codes-1-condition-flags-and-codes

Проверял. Нет, GCC не настолько крут.

https://gcc.gnu.org/onlinedocs/gcc-3.4.4/gcc/Optimize-Options.html

-funroll-loops
Unroll loops whose number of iterations can be determined at compile time or upon entry to the loop. -funroll-loops implies -frerun-cse-after-loop. It also turns on complete loop peeling (i.e. complete removal of loops with small constant number of iterations). This option makes code larger, and may or may not make it run faster.
-funroll-all-loops
Unroll all loops, even if their number of iterations is uncertain when the loop is entered. This usually makes programs run more slowly. -funroll-all-loops implies the same options as -funroll-loops.

int ar[1024];

for(size_t i = 0; i < 1024 / 4; i += 4)

{

ar[i] = ...;

ar[i + 1] = ...;

ar[i + 2] = ...;

ar[i + 3] = ...;

}

Раз уж далее речь о замене умножения сложением, то и здесь надо побитовый сдвиг применить. Ещё, если вам нужна обработка всего массива, то делить на 4 лист массива — ошибочное решение.

Видимо, вы хотели как-то вот так;

#define sz 1024
...
int ar[sz] = {0}, *pa = &ar;
size_t n4 = sz >> 2;
  while (n4-- > 0) {
    *pa++ = ... ;
    *pa++ = ... ;
    *pa++ = ... ;
    *pa++ = ... ;
  }
pa = &ar;

Чет вы куда то не туда ушли. Вот такое вот написание цикла нафиг не нужно. Чел хотел напистать обычный for() по массиву известной длинны. Тогда компилятор заранее знает сколько итераций у этого цикла и может разанроллить его на 4 или 8, как посчитает нужным.

В тупые подстановки типа сдвига вместо деления или замены деления на обратный инвариант он (компилятор) вполне умеет делать сам.

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

Выше коммент с флагами компилятора для оптимизаций циклов. Так вот с них и надо было статью начинать. Без них никаких подмен и ускорений не будет даже -O3

Кстати, подобное оформление цикла, как у автора:

for (size_t i = 0; i < 1024 / 4; i += 4)

даст обработку массива до 255 элемента, а не до 1023 включительно.

То как реализованы итераторы, как бы намекает что этот подход лучше. То есть при проходе по циклу с индексами, обычно используются инструкции для относительной адресации что обычно не быстро. В то время, как инкремент ссылки превращается в 1 регистр с add x, 4. move ptr[x], что занимает как меньше байтиков так и быстрее работает на процессоре.

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

volatile - это в первую очередь сообщение компилятору, что содержимое переменной в любой момент может измениться по внешним причинам. Например, если это регистр, связанный с оборудованием, или элемент буфера DMA, или её может изменить другой процесс (если программа работает в среде операционной системы). Соответственно, оптимизация по отношению к ней будет иной.

Особенно это хорошо проявляется на процессорах RISC с гарвардской архитектурой (ARM, RISC-V и т.п.).

Допустим, мы объявили регистр ввода-вывода обычной переменной и каким-то образом назначили ей корректный адрес размещения в памяти, отраженный на физический регистр. А теперь нам нужно дрыгнуть один раз выходом операцией "чтение-модификация-запись". То есть, прочитать регистр, изменить один бит, записать, потом снова изменить, потом снова записать.

int reg_0;
reg_0 |= 0x01;
reg_0 &= 0xFE;

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

LDR R0, REG_0 ;Чтение регистра
ORI R0, 0x01	;Изменение содержимого
ANDI R0, 0xFE	;Изменение содержимого
STR REG_0, R0	;Запись изменений

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

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

LDR R0, REG_0 ;Чтение регистра
ORI R0, 0x01	;Изменение содержимого
STR REG_0, R0	;Запись изменений

LDR R0, REG_0 ;Чтение регистра
ANDI R0, 0xFE	;Изменение содержимого
STR REG_0, R0	;Запись изменений

Это же касается и операций чтения. При каждом обращении в Сишной программе к этому регистру, будут всегда производиться чтения с физического регистра.

volatile у переменной означает, что чтение и запись этой переменной --- непосредственные элементы наблюдаемого поведения (разновидность ввода и вывода программы, соответственно, а не операции над ячейками абстрактной памяти C/C++). Невозможность тех или иных преобразований кода компилятором --- это уже следствие.

Оптимизации не могут сломать корректный код. В свою очередь, запрет оптимизаций не может сделать некорректный код корректным. Если volatile используется для запрета оптимизаций, то он используется не по назначению. Использование volatile вместо атомиков --- яркий тому пример.

Как оказалось оптимизация может сломать код.

Я сейчас пишу игру под компилятор СС65 для NES и в одном месте оптимизация ломает программу (нарушает вывод графики), а без оптимизации все нормально.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории