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

Оптимизация кода компилятором


«компилятор не будет оптимизировать эту переменную» — что означает оптимизировать? Наверное очень много людей, когда только начинали программировать задавались этим вопросом, не так ли? Думаю лучше продемонстрировать все на примерах, нежели рассказывать термины, которые большинству останутся не понятными.

Ну давайте начнем, к примеру имеем простой массив(правда не с простым размером), в цикле с которым выполняем какое-либо действие:

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)


  1. Borikinternet
    25.06.2022 19:03
    +9

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


  1. FD4A
    25.06.2022 19:08
    +7

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


    1. F0iL
      25.06.2022 23:18
      +8

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

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


  1. deema35
    25.06.2022 19:34
    -2

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

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

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


    1. aamonster
      25.06.2022 19:56
      +1

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

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


      1. deema35
        25.06.2022 20:14

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


        1. sergio_nsk
          25.06.2022 21:15
          +3

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

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


          1. aamonster
            26.06.2022 13:41

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


        1. aamonster
          26.06.2022 13:38
          +2

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

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

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

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

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

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

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

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


    1. Goron_Dekar
      25.06.2022 20:53
      +2

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

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

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


    1. deema35
      26.06.2022 01:37

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

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

      И картинка:


    1. MediaNik5
      27.06.2022 10:04

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


      1. deema35
        27.06.2022 12:57

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

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


        1. Devoter
          28.06.2022 02:34

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


  1. reatfly
    25.06.2022 19:35
    +11

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

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

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


    1. 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 компилятор не будет делать никаких оптимизаций при обращении к переменной, и всегда будет честно читать ее значение.
      Как уже отмечалось в комментариях, это используется в программировании контроллеров/..., когда значение по адресу может меняться внешним образом.


      1. vassabi
        25.06.2022 20:30
        +5

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

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

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


    1. FD4A
      25.06.2022 20:39

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


      1. RTFM13
        25.06.2022 21:23
        +4

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

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


      1. encyclopedist
        25.06.2022 22:53
        +8

        Если к этому флажку возможен доступ из разных потоков, вам нужен std::atomic<bool>


      1. aamonster
        26.06.2022 13:45
        +2

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

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


        1. FD4A
          26.06.2022 13:52

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


          1. aamonster
            26.06.2022 14:09
            +2

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


    1. Myz17
      27.06.2022 10:22

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


      1. reatfly
        27.06.2022 10:32
        +3

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


    1. ihost
      27.06.2022 16:09
      -2

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

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

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

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


      1. reatfly
        27.06.2022 16:16
        +1

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


        1. 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 уже не подходит для этого


          1. me21
            28.06.2022 08:42

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


            1. ihost
              28.06.2022 10:49

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

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

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


          1. reatfly
            28.06.2022 11:30

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

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


            1. ihost
              28.06.2022 11:54

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

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


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


  1. SGordon123
    25.06.2022 20:10

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


    1. RTFM13
      25.06.2022 23:59

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


      1. SGordon123
        26.06.2022 09:13

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


  1. Skykharkov
    25.06.2022 20:24
    -6

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


    1. FD4A
      25.06.2022 20:32
      +2

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


    1. vassabi
      25.06.2022 20:33
      +1

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

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


      1. Nick_Shl
        25.06.2022 21:35
        +1

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


        1. Skykharkov
          25.06.2022 21:51
          -4

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


          1. Nick_Shl
            25.06.2022 23:33
            +6

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


    1. gxcreator
      25.06.2022 20:51
      +3

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


  1. DustCn
    25.06.2022 20:27
    +13

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

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

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

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


    1. vassabi
      25.06.2022 20:34
      +2

      "как минимум atomic"
      вот да, плюс еще с барьерами памяти


  1. Lofer
    25.06.2022 22:54

    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.

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


    1. vassabi
      25.06.2022 23:03

      On a uniprocessor system

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

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


      1. Nick_Shl
        25.06.2022 23:42

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


        1. aamonster
          26.06.2022 13:51

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


          1. Nick_Shl
            27.06.2022 06:02

            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. Но это не точно.


    1. september669
      27.06.2022 14:28

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


  1. eptr
    26.06.2022 04:22

    const MAX_COUNT_PEOPLE = 4;

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


    1. hello_my_name_is_dany
      26.06.2022 05:17

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


    1. ZyXI
      26.06.2022 17:37

      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.


      1. eptr
        26.06.2022 23:21
        +1

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

        А C++?

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

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

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

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


  1. mentin
    26.06.2022 06:42

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


  1. Slavik_Kenny
    26.06.2022 21:38
    +1

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

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

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


  1. 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) работать не будет.


    1. Nick_Shl
      27.06.2022 06:12

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

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

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


    1. 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.


  1. gscdlr
    26.06.2022 22:57

    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;


    1. DustCn
      27.06.2022 12:36

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

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

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


  1. DX168B
    28.06.2022 11:19

    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	;Запись изменений

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