Оптимизация кода компилятором
«компилятор не будет оптимизировать эту переменную» — что означает оптимизировать? Наверное очень много людей, когда только начинали программировать задавались этим вопросом, не так ли? Думаю лучше продемонстрировать все на примерах, нежели рассказывать термины, которые большинству останутся не понятными.
Ну давайте начнем, к примеру имеем простой массив(правда не с простым размером), в цикле с которым выполняем какое-либо действие:
int ar[1024];
for(size_t i = 0; i < 1024; i++)
{
ar[i] = ...;
}
Самая затратная операция в этом примере не присваивание ячейке массива какого-либо значения и не инкремент счетчика, а именно операция сравнения, поэтому компилятор оптимизирует это примерно вот так:
int ar[1024];
for(size_t i = 0; i < 1024 / 4; i += 4)
{
ar[i] = ...;
ar[i + 1] = ...;
ar[i + 2] = ...;
ar[i + 3] = ...;
}
Еще очень простой пример, в котором имеем массив символов, с помощью цикла проходим по всей строке и выполняем какие-то действия с символами:
сhar str[125];
for(size_t i = 0; i < strlen(str); i++)
{
...
}
В этом случае компилятор вынесет вызов strlen() в отдельную переменную:
сhar str[125];
size_t length = strlen(str);
for(size_t i = 0; i < lenght; i++)
{
...
}
Также чтобы не писать код, так как он очевиден, компилятор заменяет умножение на 2, сложением, но и пожалуй самый главный пример по нашей тематике, это то, что в большинстве случаев компилятор разгружает runtime программы, путем подстановки в выражения уже их значения, к примеру мы пишем программу для лифта. Одно из условий данной программы таково, что как только зайдут к примеру больше 4 человек должно выдаться предупреждение.
const MAX_COUNT_PEOPLE = 4;
size_t countPeole = 0;
...
if(countPeople > MAX_COUNT_PEOPLE)
{
// Выдаем предупреждение
}
// Значение переменной countPeople к примеру будет менять с другого потока
Все же хорошо, ошибки невозможны в этом коде. Но по сути условие будет всегда истинно, так как компилятор уже запомнил значения этих переменных. И вот как раз таки в таких случаях применяется ключевое слово volatile, чтобы избежать подобных казусов, это будет выглядеть вот так:
const MAX_COUNT_PEOPLE = 4;
volatile size_t countPeole = 0;
...
if(countPeople > MAX_COUNT_PEOPLE)
{
// Выдаем предупреждение
}
// Значение переменной countPeople к примеру будет менять с другого потока
Заключение
Думаю мне получилось объяснить самые азы того зачем нужен volatile. Для еще лучшего понимания советую прочитать эту статью. Желаю удачи в изучение огромного мира технологий C/C++.
Комментарии (62)
FD4A
25.06.2022 19:08+7Стоит дополнить примерами того для чего мы отключаем оптимизации для переменной: регистры в периферийных устройствах; глобальные переменные, используемые в обработчиках прерываний; глобальные переменные, используемые совместно несколькими задачами в многопотоковом приложении; для дебага.
F0iL
25.06.2022 23:18+8глобальные переменные, используемые совместно несколькими задачами в многопотоковом приложении
Во многих случаях (особенно если у вас многоядерная/многопроцессорная система) использовать volatile для такого это прекрасный способ отстрелить себе ногу.
deema35
25.06.2022 19:34-2// Значение переменной countPeople к примеру будет менять с другого потока
Но ведь изменения из другого потока это внутренние изменения и компилятор о них прекрасно знает, ведь код другого потока он тоже компилирует. Тут скорее идет речь о написании драйвера.
Представьте пишите вы код для микроконтроллера но без ядра поэтому функции типа digitalWrite вы не можете использовать и тогда вы идете в даташит и находите по какому смещению в памяти находится массив переменных gpio. После этого вы производите выделение нужной вам памяти и объявляете эту память volatile .
aamonster
25.06.2022 19:56+1По стандарту – это не его дело.
И вообще, анализировать весь ваш код (часть из которого может быть вообще в другом модуле, который компилируется отдельно) – неблагодарная задача. Лучше потребовать от вас использовать примитивы синхронизации (как уже отметили, не volatile).
deema35
25.06.2022 20:14То есть вы хотите сказать компилятор может превратить глобальную переменную в константу для одного модуля поскольку он эту глобальную переменную не изменяет, а для остальных модулей оставить её переменной. Тогда у нас и в однопоточном приложении начнутся проблемы. Мы ведь не обязаны писать весь код в одном файле.
sergio_nsk
25.06.2022 21:15+3Не может. Он может прочитать один раз значение переменной в регистр при входе в функцию модуля и использовать этот регистр до конца функции. В контексте этой функции компилятор может считать прочитанное значение переменной постоянным.
Для информации,
const
не гарантирует неизменчивость значения, только гарантирует невозможность присвоения нового значения. Одна и та же область памяти может быть константой и неконстантной в разных частях кода. Поэтому компилятор обязан считать значение при первом обращении к переменной, но потом может "закешировать" считанное значение в регистре.aamonster
26.06.2022 13:41Насчёт const – да. Модификатор "const volatile" особенно прекрасен (при том, что это обычное дело на мк).
aamonster
26.06.2022 13:38+2Вам уже ответили, но вкратце основная идея: компилятор, пока явно не указано обратное, вправе считать, что код, который ему доступен, выполняется на изолированой "машине".
Т.е., допустим, у нас есть глобальная переменная v. Пусть функция foo присваивает ей какое-то значение (скажем, константу) и больше не трогает. С этого момента компилятор может считать, что в v это значение. Он может положить его туда сразу, может не класть, а просто при всех использованиях v подставлять эту константу. Даже если функция foo вызывает функцию bar, лежащую в том же модуле.
Ситуация меняется в трёх случаях:
Мы выходим из функции foo. Поскольку при этом мы можем попасть в другой модуль (другую единицу трансляции) – придётся к этому моменту записать значение в память, вдруг кому-то понадобится.
Мы вызываем функцию из другого модуля. Надо записать до вызова, прочитать после.
-
Примитивы синхронизации (mutex, к примеру). Пока мы ждём mutex – все наши переменные могут изменить снаружи. Значит, их на это время надо положить в память, а потом доставать по мере необходимости.
Нетрудно убедиться, что 1-2 решают упомянутые вами проблемы однопоточного кода. А 3 при правильном использовании – и многопоточного.
Извините за такое упрощённое описание, я упирал на понятность, а не точность. Формальные детали лучше бы читать в стандарте.
Goron_Dekar
25.06.2022 20:53+2Но ведь изменения из другого потока это внутренние изменения и компилятор о них прекрасно знает
Абсолютно не так. Другой поток может быть в другой библиотеке или, что чаще, другом объектнике и скомпиллирован другим инстансом.
Почитайте про Еденицу трансляции
deema35
26.06.2022 01:37Вот пример записи в порт атмеги:
volatile uint8_t * portd = 0x2b; *portd = 0xFF;
И картинка:
MediaNik5
27.06.2022 10:04компилятор не знает об изменениях, сделанных другим потоком. как минимум потому, что они могут случиться в любой момент времени, а не в определённый, как это происходит в текущем потоке.
deema35
27.06.2022 12:57А причем здесь время когда изменение происходит. Мы же говорим о случаях когда компилятор превращает переменную в литеру, из-за того что он считает значение этой переменной будет постоянным на протяжении выполнения всей программы. Но если факт изменения переменной существует в принципе, то переменную в литеру уже точно нельзя превращать.
Поэтому ключевое слово volatile для таких случаев не требуется.
Devoter
28.06.2022 02:34В том-то и дело, что никакого факта не существует в данном случае. Сейчас volatile имеет смысл, пожалуй, только на МК. Прерывание может сработать, а может и не сработать никогда, что, конечно, на этапе компиляции неизвестно. Таким образом, программист явно требует от компилятора каждый раз при обращении к переменной читать ее значение из памяти, а не кешировать в регистре.
reatfly
25.06.2022 19:35+11Никогда, слышите, никогда не используйте
volatile
в одном предложении сmultithreading
. Единственное исключение: предыдущее предложение.https://stackoverflow.com/questions/4557979/when-to-use-volatile-with-multi-threading
Пример из статьи, где переменная объявляется
volatile
и якобы из-за этого можно мониторить, что она изменяется в другом потоке - абсолютно неверен.reatfly
25.06.2022 19:52+5Пример использования
volatile
:volatile uint16_t* reg = 0x1234567; *reg = 1; while (*reg == 1) { // do something }
Без
volatile
компилятор просто заменит условие на `while (true)`, потому что значение было присвоено в 1, и проверяется 1. При объявлении переменной какvolatile
компилятор не будет делать никаких оптимизаций при обращении к переменной, и всегда будет честно читать ее значение.
Как уже отмечалось в комментариях, это используется в программировании контроллеров/..., когда значение по адресу может меняться внешним образом.vassabi
25.06.2022 20:30+5это вы компилятор.
а есть еще и процессор - и у него есть свои оптимизации и интересные спецэффекты
(изза которых люди открывают для себя атомарные операции и барьеры памяти :) )
FD4A
25.06.2022 20:39Делаю библиотечку в которой будет функция routine, она будет крутиться в отдельном thread в моём основном приложении. В этой функции есть флажок изменяемый только из приложения, я вот не уверен что без volatile флажок не будет соптимизирован => volatile флажок?
RTFM13
25.06.2022 21:23+4Гонять мультитредовые данные через volatile очень дурной тон в общем случае. Использование как однонаправленный флаг получше, но тоже не очень - зависит от контекста.
Где-нибудь на 8-битных м/контроллерах это приемлемо для общения с прерываниями, например (в сторону повышения приоритета), но на чем-то сложнее лучше использовать как минимум rtos и встроенные механизмы ОС - всякие семафоры, shared memory и т.п.. чтобы не изобретать велосипед.
encyclopedist
25.06.2022 22:53+8Если к этому флажку возможен доступ из разных потоков, вам нужен
std::atomic<bool>
aamonster
26.06.2022 13:45+2Не надо так. Вы ж под этим флажком наверняка будете менять какие-то данные (ну или сигнализировать флажком об их изменении) – а на них этот volatile не распространяется.
Используйте семафор.
FD4A
26.06.2022 13:52Очевидно что флажок обрамляется примитивом синхронизации. Я просто не уверен что он сам может быть не volatile. Хотя посидел на godbolt и поковырял классического производителя-потребителя, без volatile криминала не увидел.
aamonster
26.06.2022 14:09+2Если обрамляется – volatile не нужен. Мы ж под этими примитивами не то что флажки – сложные структуры данных "отдаём" в другой тред.
ihost
27.06.2022 16:09-2никогда не используйте
volatile
в одном предложении сmultithreading
Да-да, конечно, ведь лучше запрятаться за высокоуровневыми абстракциями, чтобы потом получать приложения, тормозящие даже на дорогущем оборудовании
При наличии прямых рук нет ничего лучше, чем хорошо спроектированный lockless с соответствующими volatile-ами и барьерами - тот же DPDK юзает в хвост и гриву все вышеуказанное
Для C++-ных junior-ов да, лучше использовать высокоуровневые абстракции типа std atomic-ов, но в целом для highload многопоточных приложений без volatile, барьеров, фенов, CAS-ов - никуда
reatfly
27.06.2022 16:16+1Казалось бы, а причем тут C, на котором написан DPDK, и его работа с регистрами?
ihost
28.06.2022 01:12+1Разве ж C и C++ отличаются в поведении относительно volatile?
Проверенная годами и highload-нагрузкой схема - volatile, CAS, выравнивание по линейке кеша, серийник в старших битах для обхода ABA-проблемы - и примитив для многопоточного lockless-алгоритма на X86-64 архитектуру, даже с множеством numa-нод готов - в большинстве случаев даже fence-ы не нужны
Понятно что есть красивый фасад в виде std::atomic вокруг кучи intrinsic-ов, и джунам лучше использовать его, чтобы не выстрелить себе в ногу, но в целом же это банальный синтаксический сахар, и странно говорить что volatile не подходит для многопоточности, в то время как он отлично подходит в решениях, проверенных годами
В конце концов, интринсик всего лишь или вставляет машинную инструкцию, или же инструктирует компилятор об аспектах кодогенерации (например запрещает переставлять операции в целях оптимизации, или маркирует неявную возможную замену объекта при девиртуализации в std::launder, к примеру)
Это примерно из того же разряда, как проверять свойства типов в compile time - классическим SFINAE или через темплейтную auto-лямбду? У второго подхода есть свои плюсы, но странно говорить, что старый добрый SFINAE уже не подходит для этого
me21
28.06.2022 08:42На volatile вы запишете значения в переменные в порядке a=1; b=2;, а соседний поток может увидеть сначала b, а потом а.
ihost
28.06.2022 10:49Так где выше хоть слово о том, что так `a=1; b=2;` надо делать - там же явно написано
Проверенная годами и highload-нагрузкой схема - volatile, CAS, выравнивание по линейке кеша, серийник в старших битах для обхода ABA-проблемы
При соблюдении всех требований целевой архитектуры - вполне себе рабочее решение :)
reatfly
28.06.2022 11:30Я не совсем понимаю, что вы хотите доказать. В программировании в 99,(9)% случаев использование
volatile
в контексте многопоточности - это ошибка и непонимание, как оно работает.Очевидно есть специальные случаи, когда это все нужно.
ihost
28.06.2022 11:54Я особенно ничего доказывать не хочу, мне просто слишком резанула глаз цитата про единственное исключение - все-таки исключение-то не единственное, и вполне себе можно сочетать volatile и multithreading при должно сноровке :)
Никогда, слышите, никогда не используйте
volatile
в одном предложении сmultithreading
. Единственное исключение: предыдущее предложение.
А так спору нет, более того, вне экзотических случаев, лучше вообще использовать готовенький TBB, решающий большинство проблем подо все адекватные платформы :)
SGordon123
25.06.2022 20:10про цикл как то не очевидно по моему, одна инструкция аля decfsz для счетчика и погнали, небось как всегда за один такт?
RTFM13
25.06.2022 23:59Проверка условия плоха ветвлением. Без него вся эта пачка присвоений тоже будет за 1 такт.
Skykharkov
25.06.2022 20:24-6volatile это вообще зло непонятно как пропущенное в стандарт. В С# то-же самое.
Как им правильно пользоваться, никто не знает, но на всякий случай "если меняете переменную в трэдах пишите volatile", типа не ошибетесь. А вот фиг там. Если уж приспичило написать странный код, меняющий глобальные переменные, то пишите хотя бы на mutex эту логику. Хотя применений, изменению глобальных переменных, довольно много. Например запись лог-файла из нескольких несинхронизированных потоков. Да много чего еще.vassabi
25.06.2022 20:33+1я думаю, его нужно было назвать "modified_outside_of_code" чтобы точнее передать суть.
То, что его используют еще и для передачи каких-то значений в многопоточке - это ИМХО unintended use
Nick_Shl
25.06.2022 21:35+1volatile = изменчивый. Что тут непонятного? Кто-то сам себе придумал, что volatile магическим образом помогает в мультипоточных приложениях(или просто не разобрался в вопросе) и теперь обижается.
Skykharkov
25.06.2022 21:51-4Да не проблема в "изменчивости" вовсе. Совершенно. Это как угодно можно реализовать. Просто volatile преподносится так, как будто это "золотая пуля" для переменных которым нужно меняться в разных потоках. На самом деле - фигушки. Это просто директива компилятору что оно будет меняться "атомарно". Т.е. не одновременно. Т.е. set через некий семафор\мьютекс или как угодно это назовите. Принцип тот-же. В одну единицу времени значение меняется одним потоком.
Nick_Shl
25.06.2022 23:33+6"атомарно" это как раз одновременно. Атомарность к volatile не имеет вообще никакого отношения.
gxcreator
25.06.2022 20:51+3volatile в стандарте вполне себе нужен. Другое дело, когда он неправильно используется, но в ногу выстрелить есть миллионы способов и без volatile.
DustCn
25.06.2022 20:27+13Автор, вместо того чтобы почитать умные книжки, как работает компилятор и какие бывают оптимизации начинает на ходу сочинять что то свое.
Самая затратная операция в этом примере не присваивание ячейке массива какого-либо значения и не инкремент счетчика, а именно операция сравнения, поэтому компилятор оптимизирует это примерно вот так.
Операция сравнения не является самой затратной. Откройте страницу из Интел мануала на ваш процессор и посмотрите что занимает она один такт. Оптимизация, которую выполнил компилятор в данном случае называется loop unrolling и она нужна для того, чтобы увеличить количество полезной работы за одну итерацию цикла. Грубо говоря мы уменьшаем количество проверок на конец цикла, а значит повышаем быстродействие.
volatile нужен по нескольким очевидным причинам. И главная, это запретить компилятору убирать эту переменную в результате dead code ellimination и прочих подстановок с упрощением выражений и вытаскиваний инвариантов. volatile для межпоточного взаимодействия это очень плохой пример, потому как это должен быть как минимум atomic.
Lofer
25.06.2022 22:54volatile это вообще зло непонятно как пропущенное в стандарт. В С# то-же самое. Как им правильно пользоваться, никто не знает, но на всякий случай "если меняете переменную в трэдах пишите 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.
Типовая проблема - типовое решение.
vassabi
25.06.2022 23:03On a uniprocessor system
и вот именно с этого нужно начинать про т.н. "volatile для многопоточности" : "когда-то, когда в настольных компьютерах был только один процессор ...."
Потому что сейчас чтобы найти такое у массового пользователя мобилок\ноутов\десктопов - это уже надо сильно постараться (мне кажется уже скоро в ардуину будут пихать многопроц)
Nick_Shl
25.06.2022 23:42Мультипроцессорность тут вообще не причем. Имел проблему с получением данных в STM32, которые через DMA складывались в память, но в ядре был включен кэш и этих данных было не видно. А что бы было видно, надо или кэш отключить, или в прерывании DMA инвалидировать область памяти буфера в кэше что бы данные грузились из памяти.
aamonster
26.06.2022 13:51О, а можно чуть-чуть подробностей? (достаточно номера мк и пары слов для поиска в datasheet)
Nick_Shl
27.06.2022 06:02https://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. Но это не точно.
september669
27.06.2022 14:28Не знаю как в C# и C++, но в яве volatile это про барьер happens before, а вовсе не про кэши
eptr
26.06.2022 04:22const MAX_COUNT_PEOPLE = 4;
Каков тип этой постоянной?
ZyXI
26.06.2022 17:37C позволяет опускать
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.eptr
26.06.2022 23:21+1C позволяет опускать
int
во многих случаях.А C++?
Статья называется "Ключевое слово «volatile» C/C++".
Это -- один момент.
Второй момент связан с тем, что под
if
'ом идёт сравнение на меньше/больше знакового с беззнаковым, на что компиляторы как правило выдают предупреждение.Причём, ещё и размеры этих знаковых беззнаковых могут отличаться...
mentin
26.06.2022 06:42Ну если уж придираться ко всем примерам, то и к вынесению strlen за пределы цикла много вопросов. Сделать это можно очевидно не всегда, а точнее довольно редко. Компилятору для этого надо доказать, что строка не может быть изменена внутри цикла, включая вызываемые функции которым передаются указатели на ее содержимое.
Slavik_Kenny
26.06.2022 21:38+1const MAX_COUNT_PEOPLE = 4; size_t countPeole = 0; ... if(countPeople > MAX_COUNT_PEOPLE) ...
Все же хорошо, ошибки невозможны в этом коде. Но по сути условие будет всегда истинно, так как компилятор уже запомнил значения этих переменных.
Я чего-то не понял, или все-таки условие всегда будет ложно?
gscdlr
26.06.2022 21:40компилятор оптимизирует это примерно вот так:
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) работать не будет.
Nick_Shl
27.06.2022 06:12Чем же инструкция сравнения "дороже"любой другой операции?
Да не дороже она. А в некоторых случаях вообще "бесплатная" - в ARM например сама операция декремента может установить флаг, а дальше функция условного перехода использовать этот флаг.
reatfly
27.06.2022 10:38Проверял. Нет, 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.
gscdlr
26.06.2022 22:57int 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;
DustCn
27.06.2022 12:36Чет вы куда то не туда ушли. Вот такое вот написание цикла нафиг не нужно. Чел хотел напистать обычный for() по массиву известной длинны. Тогда компилятор заранее знает сколько итераций у этого цикла и может разанроллить его на 4 или 8, как посчитает нужным.
В тупые подстановки типа сдвига вместо деления или замены деления на обратный инвариант он (компилятор) вполне умеет делать сам.
Когда же вы руками начинаете разворачивать цикл, делаете арифметику с указателями и прочие не очевидные штуки, то первым делом у компилятора ломается нежный и ранимый pointer disambiguator, тот кусочек, который определяет что вот эти два указателя независимы, и могут считаться разными переменными, а вот эти два указателя могут указывать на пересекающие части памяти и с ними нужно очень осторожно. Короче у него просто съедет крыша и все ваши указатели он будет считать опасными и значит количество возможных оптимизаций с ними упадет до нуля.
DX168B
28.06.2022 11:19volatile - это в первую очередь сообщение компилятору, что содержимое переменной в любой момент может измениться по внешним причинам. Например, если это регистр, связанный с оборудованием, или элемент буфера 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 ;Запись изменений
Это же касается и операций чтения. При каждом обращении в Сишной программе к этому регистру, будут всегда производиться чтения с физического регистра.
Borikinternet
Я бы уточнил, что, например, переменная
countPeople
может быть изменена не только программой, а и контроллером лифта. Это и будет то самое "извне", которое должен уважать компилятор.