Action information
229,80
рейтинг
6 декабря 2012 в 13:34

Разработка → Однажды вы читали о ключевом слове volatile… tutorial

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

Сегодня рассмотрим менее экзотический сценарий использования ключевого слова volatile.



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

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

for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
     *ptr;
}

Этот код проходит по всей области и читает по одному байту из каждой страницы памяти. Одна проблема – компилятор этот код оптимизирует и полностью удалит. Имеет полное право – этот код не влияет на наблюдаемое поведение. Ваши переживания о выделении страниц операционной системой и вызванных этим задержке к наблюдаемому поведению не относятся.

Что же делать, что же делать… А, точно! Давайте мы запретим компилятору оптимизировать этот код.

#pragma optimize( "", off )
for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
     *ptr;
}
#pragma optimize( "", on )

Отлично, в результате…

1. использована #pragma, которая делает код плохо переносимым, плюс…
2. оптимизация выключается полностью, а это увеличивает объем машинного кода в три раза, плюс в Visual C++, например, эта #pragma может быть использована только снаружи функции, соответственно, рассчитывать на встраивание этого кода в вызывающий код и дальнейшую оптимизацию тоже не приходится.

Здесь отлично помогло бы ключевое слово volatile:

for( volatile char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
     *ptr;
}

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

Теперь попробуем перезаписать память во имя безопасности и паранойи (это не бред, вот как это бывает в реальной жизни). В том посте упоминается некая волшебная функция SecureZeroMemory(), которая якобы гарантированно перезаписывает нулями указанную область памяти. Если вы используете memset() или эквивалентный ей написанный самостоятельно цикл, например, такой:

for( size_t index = 0; index < size; index++ )
     ptr[index] = 0;


для локальной переменной, то есть риск, что компилятор удалит этот цикл, потому что цикл не влияет на наблюдаемое поведение (доводы в том посте к наблюдаемому поведению тоже не относятся).

Что же делать, что же делать… А, мы «обманем» компилятор… Вот что можно найти по запросу “prevent memset optimization”:

1. замена локальной переменной на переменную в динамической памяти со всеми вытекающими накладными расходами и риском утечки (сообщение в архиве рассылки linux-kernel)
2. макрос с ассемблерной магией (сообщение в архиве рассылки linux-kernel)
3. предложение использовать специальный символ препроцессора, который запрещает встраивание memset() по месту и затрудняет компилятору оптимизацию (естественно, такая возможность должна быть поддержана в используемой версии библиотеки, плюс Visual C++ 10 умеет оптимизировать даже код функций, помеченных как не подлежащие встраиванию)
4. всевозможные последовательности чтения-записи с использованием глобальных переменных (кода становится заметно больше и такой код не потокобезопасен)
5. последующее чтение с сообщением об ошибке в случае, если считаны не те данные, что были записаны (компилятор имеет право заметить, что «не тех» данных оказаться не может, и удалить этот код)

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

Вы можете скомпилировать функцию перезаписи в отдельную единицу трансляции, чтобы компилятор «не увидел», что она делает. После очередной смены компилятора в игру вступит генерация кода линкером (LTCG в Visual C++, LTO в gcc или как это называется в используемом вами компилятором) – и компилятор прозреет и увидит, что перезапись памяти «не имеет смысла», и удалит ее.

Не зря появилась поговорка you can’t lie to a compiler.

А что если посмотреть на типичную реализацию SecureZeroMemory()? Она по сути такая:

volatile char *volatilePtr = static_cast<volatile char*>(ptr);
for( size_t index; index < size; index++ )
        * volatilePtr = 0;
}

И все – компилятор более не имеет права удалять запись…

КРАЙНЕ НЕОЖИДАННО… вопреки всем суевериям зачеркнутое утверждение выше неверно.

На самом деле – имеет. Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором volatile. Вот для таких:

volatile buffer[size];

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

buffer[size];
SecureZeroMemory(buffer, sizeof(buffer));

Вся надежда на разработчиков компилятора – в настоящий момент и Visual C++, и gcc не оптимизируют обращения к памяти через указатели с квалификатором volatile – в том числе потому, что это один из важных сценариев использования таких указателей.

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

Причина этому банальна – это «не нужно».

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

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

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

volatile – не только для драйверов и операционных систем.

Дмитрий Мещеряков,
департамент продуктов для разработчиков
Автор: @DmitryMe
ABBYY
рейтинг 229,80
Action information

Комментарии (84)

  • –3
    Гарантированный стандартом способ обнулить все елементы массима, это сделать вот так

    int buffer[size] = {0};

    И не нужно memset-ов ни каких
    • +13
      Вот что думает об этом Visual C++ 10

         304: int _tmain(int /*argc*/, _TCHAR* /*argv[]*/)
         305: {
         306: 	const size_t size = 100;
         307: 	int buffer[size] = {0};
         308: 	return 0;
      00403940  xor         eax,eax  
         309: }
      00403942  ret  
      


      Нетрудно видеть, что сгенерирован только код, обеспечивающий «return 0;». И такое поведение полностью соответствует Стандарту.
      • +10
        Не путайте теплое с мягким, компилятор здесь вообще ваш массив выпилил за ненедобностью. Если бы он использовался, то он сгенерил бы код для инициализации нулями
        • +3
          Это вы не путайте теплое с мягким. В примере ниже массив используется. Покажете код memset в примере ниже?

             304: int _tmain(int /*argc*/, _TCHAR* /*argv[]*/)
             305: {
          00403940  sub         esp,194h  
          00403946  mov         eax,dword ptr [___security_cookie (407018h)]  
          0040394B  xor         eax,esp  
          0040394D  mov         dword ptr [esp+190h],eax  
          00403954  push        esi  
          00403955  push        edi  
             306: 	const size_t size = 100;
             307: 	int buffer[size] = {0};
             308: 	for( size_t i = 0; i < size; i++ ) {
          00403956  mov         edi,dword ptr [__imp__rand (4050C0h)]  
          0040395C  xor         esi,esi  
          0040395E  mov         edi,edi  
             309: 		buffer[i] = rand();
          00403960  call        edi  
          00403962  inc         esi  
          00403963  cmp         esi,64h  
          00403966  jb          wmain+20h (403960h)  
             310: 	}
             311: 	memset( buffer, 0, sizeof( buffer ) );
             312: 	return 0;
             313: }
          00403968  mov         ecx,dword ptr [esp+198h]  
          0040396F  pop         edi  
          00403970  pop         esi  
          00403971  xor         ecx,esp  
          00403973  xor         eax,eax  
          00403975  call        __security_check_cookie (403BCEh)  
          0040397A  add         esp,194h  
          00403980  ret
          
          • +8
            И в этом примере массив тоже не используется. Компилятор, естественно, не заполняет массив нулями, если может доказать что никто никогда их не прочитает.

            А вот пример когда массив используется. Показать memset?

            void f(void*);

            void g() {
            int buffer[100] = {0};
            f(buffer);
            }

            void g() {
            0: sub $0x198,%rsp
            int buffer[100] = {0};
            7: xor %eax,%eax
            9: mov $0x32,%ecx
            e: mov %rsp,%rdi
            11: rep stos %rax,%es:(%rdi)
            f(buffer);
            14: mov %rsp,%rdi
            17: callq 1c <_Z1gv+0x1c>
            18: R_X86_64_PC32 _Z1fPv-0x4
            }
            1c: add $0x198,%rsp
            23: retq
            • –3
              Замечательно. Между тем пост (более-менее в основном) о перезаписи массива перед выходом из функции. Где в вашем коде перезапись массива перед выходом из функции g()?
              • 0
                Не съезжйте с темы. Вам показали а) цитату из стандарта б)правильный пример где компилятор не оптимизирует инициализацию нулями.

                В следующий раз пишите на темы, в которых вы ориентируетесь.
                • +7
                  Так-так, давайте не будем переходить на личности.
                  В тексте статьи ведь указано конкретное применение этого всего: гарантировать выделение и обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать, дабы избежать накладных задержек на выделение и ошибок связанных с отсутствием обнуления. Тут автор прав.
                  • +1
                    Вызов любой функции с передачей туда массива сразу говрит, что оптимизировать ничего нельзя.

                    Оптимизировать он будет только если а) массив никуда не передается б) не ЧИТАЕТСЯ локально. Если код только пишет в массив, но не читает, ничего инициализировать компилятор не будет, т.к. не смыла.

                    Соответсвенно проблема «обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать» имеет чисто академический интерес.
                    • +1
                      Всё ещё недостаточно условий. Если массив не убегает (escapes), то компилятор всё равно может соптимизировать, например, при помощи SROA:
                      $ cat /tmp/a.c
                      int g();
                      int f()
                      {
                        int x[4] = {0};
                        x[1] = g();
                        x[2] = g();
                        return x[1] + x[2] + x[3];
                      }
                      $ clang -emit-llvm -S -O2 /tmp/a.c
                      $ cat a.s
                      [...]
                      define i32 @f() nounwind uwtable {
                      entry:
                        %call = tail call i32 (...)* @g() nounwind
                        %call1 = tail call i32 (...)* @g() nounwind
                        %add = add nsw i32 %call1, %call
                        ret i32 %add
                      }
                      
                      declare i32 @g(...)
                      

                      Как видно, массива как таквого в промежуточном коде нет.
                      • –5
                        Ну это отдельный случай, сланг здесь вообще решил использовать три отдельные переменные, вместо массива. Третья переменная будет нулем, т.к. она «default initalized»

                        Мы же обсуждаем инициализацию массивов.
                        • +2
                          С точки зрения программиста — это массив. А с точки зрения машинного кода это скорее всего будет только два регистра. Понимаете, инициализация не-volatile данных не наблюдаема и поэтому может быть что угодно пока с точки зрения программиста всё выглядит неотличимо.
                          • –3
                            Не понятно за что минус. Я с вашей позицией и не спорю. Мой изначальный посыл был «форсированная инициализация нулями не имеет смысла». Да, есть случаи, когда память нужно затереть из соображений безопасности, но это отдельная тема и делается это далеко не нулями.
                            • +1
                              Какая разница — нулями или не нулями перезаписывать память?
                              • 0
                                Большая разница, если хитриый руткит захочет выловить пароль из вашей программы, он в первую очередь будет перехватывать попытки обнулить участки памяти. Т.к. перехватывать любые перезаписи сильно сложнее и дороже.
                                • +2
                                  Тут можно поспорить.

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

                                  Перезапись чем-то нетривиальным может быть выявить проще, потому что она делается сложнее и реже и благодаря этому соответствующий ей шаблон проще искать.
                            • +2
                              > Да, есть случаи, когда память нужно затереть из соображений безопасности

                              >> Соответсвенно проблема «обнуление памяти «здесь и сейчас», вне зависимости от того, кто там дальше будет (или не будет) в неё писать» имеет чисто академический интерес.

                              Мне кажется вы немного себе противоречите.
            • 0
              Вы уверены что LTCG не выпилит и вызов функции?
              • 0
                Edited: +1, вполне может и заинлайнить.

                • +1
                  Я вообще то именно об этом говорю: код на который я ответил «гарантирует» генерацию в надежде что если тело функции недоступно в текущей единице трансляции, то оно не может быть заинлайнено и удалено если оно пустое или нерелевантное.
                  • +1
                    Может и в этом случае не выпилить, если в f() этот массив нетрививальным образом используется, например — убегает куда-нибудь в системную библиотеку. Я показывал именно ассемблерный листинг одного объектного файла — там без вариантов должна быть инициализация.

                    А вообще автор статьи прав, Gunnar какую-то ересь понес к делу не относящуюся и увел тему в сторону :)
    • +2
      Минусуюшим, цитирую стандарт

      8.5:
      To zero-initialize storage for an object of type T means:

      if T is a scalar type, the storage is set to the value of 0 (zero) converted to T;
      if T is a non-union class type, the storage for each nonstatic data member and each base-class subobject is zero-initialized;
      if T is a union type, the storage for its first data member is zero-initialized;
      if T is an array type, the storage for each element is zero-initialized;
      if T is a reference type, no initialization is performed.

      To default-initialize an object of type T means:

      if T is a non-POD class type, the default constructor for T is called
      if T is an array type, each element is default-initialized;
      otherwise, the storage for the object is zero-initialized.

      8.5.1:

      If there are fewer initializers in the list than there are members in the aggregate, then each member not explicitly initialized shall be default-initialized (8.5).

      8.5.1 говорит, что если инциализаторов меньше чем елементов, то остальные елементы «hall be default-initialized (8.5).».

      8.5. говрит, что для массовов, каждый елемент " is default-initialized"
      Далее для всех POD типов «otherwise, the storage for the object is zero-initialized

      Учтите матчасть, господа
      • +3
        Это не наблюдаемое поведение. Компилятор имеет право сделать ну, например, SROA (scalar replacement of aggregates), и разбить ваш массив на несколько различных переменных, лежащих даже не в соседних ячейках памяти, а несипользуемые ячейки вообще не распределять в памяти (конечно же если адрес этого массива никуда не убегает (escapes)).

  • +3
    for( char* ptr = start; ptr < start + size; ptr += MemoryPageSize ) {
         *ptr;
    }


    Ага, да. Внезапно DR 1054:
    www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1054

    C and C++ differ in the treatment of an expression statement, in particular with regard to whether a volatile lvalue is fetched. For example,


        volatile int x;
        void f() {
            x;    // Fetches x in C, not in C++
        }


    В C++11 исправлено.
    • 0
      В первом куске кода char* или volatile char*?
      • 0
        Первый кусок кода скопирован из статьи как есть. Но очевидно что автор имел ввиду volatile.
        • 0
          В посте три похожих куска. Первые два без volatile, третий — с volatile. У вас во втором фрагменте кода volatile int x; и в цитате после нее говорится про volatile lvalue, так что я предположил, что вы имели в виду volatile char*, иначе не понятно, в чем именно интрига.
          • 0
            Пересмотрел пост — теперь вижу. Да, там есть кусок кода с volatile char* и я скопировал не тот кусок. Да, в моём комментарии имелось ввиду volatile char*.
  • 0
    > во имя безопасности и паранойи

    Если вспомнить как второе слово последнее время используют, думаю их пора признать синонимами.
    • 0
      Не совсем. Безопасность — вещь комплексная. Да, полезно перезаписывать переменные, использованные под хранение секретных данных, но перезапись не делает весь продукт абсолютно безопасным, просто немного поднимает планку.
  • +3
    "Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором volatile.

    Если сами данные не имеют квалификатора volatile, а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению.
    "
    Откуда такая трактовка? Пусть через указатель, но мы меняем именно данные с квалификатором volatile, соответственно это должно быть наблюдаемое поведение. Более того тогда как трактуется «volatile char[100] a»?
    • 0
      Откуда такая трактовка? Пусть через указатель, но мы меняем именно данные с квалификатором volatile, соответственно это должно быть наблюдаемое поведение.

      Такая трактовка от того, что после очень тщательных поисков мне так и не удалось найти, где именно Стандарт гарантирует доступ к не-volatile данным через указатель на volatile.

      Обратите внимание, я буду рад оказаться неправым в этой части поста, потому что это будет означать, что использование указателей на volatile — просто идеальное решение рассмотренной изначальной проблемы.

      Более того тогда как трактуется «volatile char[100] a»?

      Visual C++ трактует это как синтаксически неверный код. Видимо, вы имели в виду что-то другое.
      • 0
        Да, я естественно имел в виду «volatile char a[100];»
        Возникает вопрос «a[0]=0» — наблюдаемо? А "*a=0"?
        А так:
        volatile char *b;

        b[1]=0;

        А чем это от предыдущего отличается?
        Я это имел в виду.
      • 0
        >>где именно Стандарт гарантирует доступ к не-volatile данным через указатель на volatile

        Ну вот представьте, что у вас есть код

        test.cpp
        void f (volatile char* buf)
        {
         // do something
        }
        
        

        а в main.cpp вы ее вызываете

        
        char buf* = something();
        f ((volatile char*)buf);


        Компилируя test.cpp, компилятор не знает и не может знать, был оригинальный указатель volatile или нет, он занет только что ему передали volatile и рабоать он с ним будет исходя из этого.
        • 0
          Компилируя test.cpp, компилятор не знает и не может знать

          Может при использовании LTCG или ее аналогов.
          • 0
            Все равно не может, вдруг вы собираетесь вашу функцию завернуть в бибилиотеку и вызывать из сторонней программы.
            • 0
              Может, если и на вызывающей стороне, и на вызываемой используется LTCG, — в этом случае он будет «видеть устройство» и того кода, и другого.
              • 0
                каким образом? Я написал библиотеку (dll) и дал вам в скомпилированном виде. Дальше что?
                • 0
                  В этом случае, естественно, компилятор не видит устройство вызываемого кода при компиляции вызывающей стороны. Я имел в виду, например, случай, когда вызываемый код находится в статической библиотеке, которая скомпилирована с LTCG.
        • +1
          Именно, а значит никаких чудес — каждое чтение и присваивание в buf будет выполнятся «честно». И даже не важно в разных это файлах или в одном — имеем преобразование из non-volatile в volatile и с этого момента компилятор будет работать с данным указателем, как с volatile. Не пойму, откуда у автора статьи такие странные выводы и паника.
      • 0
        И, кстати, мы про какой стандарт говорим? Я пока что нашёл только упоминание о том, что доступ к volatile переменным, полям, метода и классам не должен порождать side-effects и тому подобное. Доступ подразумевает именно доступ — не важно через указатель или непосредственно к переменной.
      • 0
        А где стандарт «не гарантирует»? Откуда такие выводы-то?
        • 0
          Насколько мне известно, по умолчанию, если в Стандарте что-то явно не указано, то это не гарантируется.
          • +1
            Дык, ещё раз — про который из стандартов мы говорим и почему не гарантируется-то? В 98 — гарантируется. В драфте 2005 — гарантируется. В том смысле что и там и там говорится «access» и соответственно, совершенно по фиг как именно осуществляется доступ к переменной/объекту — непосредственно или через указатель. К сожалению 2003 под рукой нет, но что-то мне подсказывает что там тоже будет написано «access».
            По Вашей логике так и const, тоже должен изменяться через указатель. И вообще никакие модификаторы не могут работать — они в стандарте совершенно одинаково описаны.
          • 0
            Я когда про массивы спрашивал именно на это и намекал — нет разницы, как осуществляется доступ — через указатель или непосредственно, а значит поведение должно быть одинаковым.
          • 0
            Не поленился, нашёл 2003 (текущий) стандарт. Там немного по-другому, действительно.
            Там написано про наблюдаемое поведение при записи чтении volatile данных. С какого бодуна запись через указатель перестала быть записью?
            • 0
              Не перестала, вопрос в том, где говорится, что использование указателя на volatile для доступа к данным, которые не объявлены как volatile, требует обращения такого же обращения с этими данными, как будто они объявлены volatile.
              • +1
                В ISO/IEC 9899 это говорится в параграфе 6.5.3.2, п. 4:

                The unary * operator denotes indirection. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’.


                То есть тип результата разыменования выводится исключительно из типа разыменуемого указателя, и никакие иные соображения во внимание не принимаются.
          • 0
            Более того, там описаны правила приведения к volatile — никаких сюрпризов, описание чего угодно можно в любой момент привести к volatile и доступ будет осуществляться с учётом этого модификатора. А вот про обратное преобразование ясно сказано, что приведение voltaile к non-volatile допустимо, но результат такого преобразования неизвестен.
  • +1
    volatile char *volatilePtr = static_cast<volatile char*>(ptr);


    Не спора ради, а просто интересуюсь. А разве такое объявление не означает, что volatile будут именно данные по указателю, а не сам указатель?
    • 0
      Этот вызывающий вопросы кусок поста написан в предположении, что в случае, когда компилятор точно знает, что этот указатель указывает на данные, которые не объявлены volatile, он имеет право считать их не volatile.
      • +1
        Как-то это очень умно для компилятора получается. Так он и на запись в конст, который на самом деле не конст мог бы не ругаться. :) Изначально, если мы сказали компилятору что это указатель на тип такой-то и более того вызывали каст — усомниться в наших действиях он не должен. А уж если во время запуска что-то поломается — это не его вина, ему так сказали сделать.
  • +1
    Вот мне интересно, зачем вообще компилятор выпиливает мёртвый код? Я вижу две причины наличия мёртвого кода в исходниках: ошибка разработчика или явное намерение разработчика. В обоих случаях достаточно выдать варнинг и не удалять мёртвый код. Ошибку разработчик исправит, а при явном намерении явно отключит варнинг директивой. Или я неправ?
    • +1
      Рассмотрим, например, std::vector. Когда он разрушается или из него удаляются элементы, для этих элементов вызываются деструкторы, скорее всего, для этого используется цикл. Очевидно, если элементы такого типа, что у них деструкторы тривиальны, то деструкторы вызывать не нужно и цикл как таковой тоже не нужен. Способность компилятора удалять мертвый код позволит вам просто написать этот цикл и расчитывать на то, что компилятор в каждом конкретном случае сможет правильно решить, нужен этот цикл или нет.
    • +1
      Нет, вы не правы. Мертвый код может получаться в результате подстановки inline функций и анализа условий, в которых они вызываются. В частности это касается STL, где используются огромные синтаксические конструкции из нескольких вариантов алгоритма и описания, когда какой вариант использовать. Лишние варианты компилятор выкидывает и остается оптимальный и легковесный код.
      • 0
        Спасибо за объяснение. Да, в плюсах действительно много генерированного кода, я как-то не подумал.
    • +2
      зачем вообще компилятор выпиливает мёртвый код

      Ошибку разработчик исправит

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

      + такой код может появлять в результате работы самого компилятора, причем довольно часто.
      • 0
        Ну для того я и предлагаю выдавать варнинг, чтобы нашёл :-) Подсветка в редакторе — это тот же варнинг.
    • 0
      Мертвый код может вполне нормально получаться, если используем библиотеки. Хотя это и дурной тон. Например недавно я написал себе шикарную библиотечку на все случаи жизни. Некоторый функции были static inline в хедере. Так оно меня ворнингами о мертвом коде задолбало. Ясен перец, что он в данном конкретном случае мертвый — библиотека-то универсальная.
    • +2
      Любая шаблонная магия порождает сотни тонн мёртвого кода.
  • +2
    volatile char *volatilePtr = static_cast<volatile char*>(ptr);
    * volatilePtr = 0;
    И все – компилятор более не имеет права удалять запись…
    КРАЙНЕ НЕОЖИДАННО… вопреки всем суевериям зачеркнутое утверждение выше неверно.


    Ожидал, что модификатор volatile работает как const, т.е. различает конструкции
    volatile char * ptr;
    и
    char * volatile ptr;


    В первом случае должен обеспечиваться доступ к памяти по указателю, во втором случае — к памяти, хранящей указатель.

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

    Для
    void x1(char * z) { *z; }

    Имеем код
    retn


    Для
    void x1(volatile char * z) { *z; }

    Код
    mov  al,[ecx]
    retn


    Для
    void x1(char * volatile z) { *z; }

    Код
    push ecx
    mov  [esp],ecx
    mov  eax,[esp]
    pop  ecx
    retn


  • +1
    Обычно описание ключевого слова сразу приводит пример с данными, которые могут быть в любой момент изменены из другой нити, аппаратным обеспечением или операционной системой.


    Использовать volatile для переменных, к которым возможен доступ из нескольких потоков — это хороший способ прострелить себе ногу. Для этой цели есть atomic, а использование volatile почти всегда неверно.
    • –1
      Ну-ну. Если я понимаю на каких архитектурах оно будет работать и знаю, как оно работает, то всё нормально. Позволяет круто заоптимизировать код. А если говорить о всяком низкоуровневом программировании и контроллерах, то там без этого вообще никак — высокоуровневые средства синхронизации либо отсутствуют либо потребляют непозволительное количество ресурсов.
      Если я знаю, что кэш когерентен (или отсутствует) и, что выравнивание не будет отключаться — то всегда пожалуйста.
      • +5
        … а также досконально знаешь свой компилятор, или просто веришь что он простит тебе undefined behavior. Аккуратно анализируешь возможные переупорядочения инструкций вокруг каждого такого volatile (как компилятором так и процессором), и можешь обосновать что все они тебе подходят.

        А на практике, наверное, просто показываешь пальцем в экран и говоришь — «ну ведь работает же!» :)
  • –6
    Вот сижу я и думаю… Pragma… volatile… большинство индусов (не в обиду нормальным, к сожалению, таких крайне мало) плюют на это, для них нижеследующее будет вЕрхом оптимизации:

    void *something(void *shi_t)
    {
        // do something
        return "false";
    }
    
    int main(void)
    {
        if (something(0x0) == "false")
            printf("false\n");
        else
            printf("true\n");
    
        return 0;
    }
    

  • –1
    Если вы используете memset() или эквивалентный ей написанный самостоятельно цикл, например, такой:

    для локальной переменной, то есть риск, что компилятор удалит этот цикл, потому что цикл не влияет на наблюдаемое
    поведение (доводы в том посте к наблюдаемому поведению тоже не относятся).


    Если локальная переменная сначала чистится, а затем используется, и такое поведение устойчиво воспроизводится то, как мне кажется, это повод написать bug-report. Ведь получается, что компилятор явно влияет на поведение программы.
    • 0
      Здесь речь о том, что она сначала используется, а потом ее неплохо бы почистить.
  • 0
    А что если посмотреть на типичную реализацию SecureZeroMemory()? Она по сути такая:
    volatile char *volatilePtr = static_cast<volatile char*>(ptr);
    for( size_t index; index < size; index++ )
            * volatilePtr = 0;
    }
    

    … Стандарт говорит, что последовательность чтения-записи должна сохраняться только для данных с квалификатором volatile. Вот для таких:
    volatile buffer[size];
    

    Если сами данные не имеют квалификатора volatile, а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению

    Конечно. Потому, что
    volatile char a[M]
    
    и
    volatile char *b
    
    это разные типы!

    А вот этот, тот же:
    volatile T (&arr)[M]
    
    Так зачем в функции принимать с типом указателя, когда можно принять с типом volatile-массива?

    Зачем нужна SecureZeroMemory(), когда можно использовать такую функцию абсолютно кроссплатформенную?
    // Think that another thread reads this array
    template<typename T, size_t M>
    inline void zero_func(volatile T (&arr)[M]) {
        for(auto &i : arr) i = 0;
    }
     
    template<typename T, size_t M>
    inline void zero_func(volatile T *ptr) {
        zero_func(reinterpret_cast<volatile T (&)[M]>(*ptr));
    }
    


    Подробнее на ideone.com
    • 0
      Обратите внимание, что массив передается по ссылке, так что все проблемы с действием квалификаторов остаются, поскольку это «ссылка на volatile».
      • 0
        Только у меня не ссылка на volatile, а volatile-ссылка на статичный массив.
        А можно цитату из стандарта, где говорится о том, что volatile-ссылка на объект не гарантирует volatile-поведение?
        И чем тогда вообще отличается поведение volatile-ссылка от не volatile ссылки?
        • 0
          А можно цитату из стандарта, где говорится о том, что volatile-ссылка на объект не гарантирует volatile-поведение?
          В Стандарте именно такого требования нет, но из этого ровно ничего не следует. Например, в Стандарте не требуется, чтобы запись через неинициализированный указатель приводила к AV (или segmentation fault — кому как нравится).
          • 0
            int main() {
             char arr1[10];
             typedef decltype(arr1) T;
             T & arr2 = arr1;
             T volatile & arr3 = arr1;
             
             char *ptr1;
             char * volatile ptr2 = ptr1;
             char volatile *ptr3 = ptr1;
             char volatile * volatile ptr4 = ptr1;
             
             return 0;
            }
            

            В этом коде нет одинаковых типов.
            А чем отличается поведение T & и T volatile &?
            И чем отличается поведение char * volatile, от char volatile *, и от char volatile * volatile?
            • 0
              Применительно к обязательности перезаписи данных — Стандарт не дает никаких гарантий ни в одном из перечисленных случаев, потому что сам массив не имеет квалификатора volatile. Плюс в примерах с указателями у вас неопределенное поведение, потому что нельзя инициализировать указатель значением неинициализированного указателя.
              • 0
                Хорошо, пусть указатели буду инициализированы, хотя сути касаемо volatile это не меняет:
                int main() {
                 char arr1[10];
                 typedef decltype(arr1) T;
                 T & arr2 = arr1;
                 T volatile & arr3 = arr1;
                 
                 char *ptr1 = new char[20];
                 char * volatile ptr2 = ptr1;
                 char volatile *ptr3 = ptr1;
                 char volatile * volatile ptr4 = ptr1;
                 delete[] ptr1;
                 
                 return 0;
                }
                

                Т.е. на вопрос: чем отличается поведение T & и T volatile &, и чем отличается поведение char * volatile, от char volatile *, и от char volatile * volatile, ваш ответ — ничем не отличаются?

                К слову, как раз наоборот, если изначально указатель/массив был с гарантией перезаписи volatile, то эта гарантия обязана распространятся и на указатели/ссылки которым он присваивается. Если мы попытаемся это обойти, то получим ошибку компилятора. И нам не поможет ни один из кастов, даже reinterpret_cast.
                int main() {
                 char volatile * volatile ptr4 = new char[20];
                 char * ptr1 = reinterpret_cast<char *>(ptr4); // error
                 delete[] ptr4;
                 
                 return 0;
                }
                

                Обратное же вполне возможно, компилятор позволяет дать гарантию volatile:
                int main() {
                 char * ptr1 = new char[20];
                 char volatile * volatile ptr4 = ptr1;
                 delete[] ptr1;
                 
                 return 0;
                }
                

                Я пока что просто намекаю, что если бы гарантия volatile противоречила изначальному отсутствию volatile или не имела эффекта в одном из случаев char volatile * или char * volatile, то компилятор бы выдавал ошибку, как и в предыдущем случае с reinterpret_cast.

                Я к тому, что вы слишком вольно трактуете стандарт, и в вашей цитате:
                volatile char *volatilePtr = static_cast<volatile char*>(ptr);
                
                Если сами данные не имеют квалификатора volatile, а квалификатор volatile добавляется указателю на эти данные, чтение-запись этих данных уже не относится к наблюдаемому поведению

                Вы добавили квалификатор volatile не указателю на данные, а данным, на который указывает указатель.
                А вот чтобы добавить квалификатор volatile указателю на данные, надо писать:
                char * volatile  volatilePtr = static_cast<char * volatile>(ptr);
                
                это все разные типы с разным поведением.
                • 0
                  Вы добавили квалификатор volatile не указателю на данные, а данным, на который указывает указатель.

                  Не самим данным (переменной), а l-value. Поменять объявление переменной невозможно, а именно наличие квалификатора volatile у самой переменной определяет, является ли она «volatile data», изменение которых относится к наблюдаемому поведению.

                  Даже если я неверно трактую Стандарт, то я его трактую слишком строго, а не слишком вольно, потому что я утверждаю отсутствие гарантии.
                  • 0
                    Для volatile char arr[10]; и volatile int a; выражения arr[1] и a — это тоже l-value. Нельзя дать l-value квалификатор volatile, но не дать его данным этой переменной — это не имеет никакого смысла.
                    Из вашего утверждения следует, что не существует случаев, когда был бы смысл использовать volatile T *ptr вместо T *ptr, и вы не сможете привести даже примера имеющего какой-то смысл?
                    Потому, что уже в выражении volatile T *ptr = new T; мы даем volatile не самим данным (переменной), а l-value-переменной. Ещё интересней вывод, что в куче вообще нельзя иметь volatile данные :)

                    Тогда квалификатор volatile, пройдя через несколько стандартов, имел бы запрет на компиляцию volatile T *.

                    С излишней строгостью вы на создаете себе проблем, которых не существует.
                    • 0
                      Мало того, lvalue/rvalue является свойством не объектов(переменных), а выражений :)
                    • +1
                      Ещё интересней вывод, что в куче вообще нельзя иметь volatile данные :)
                      Отчего же, отчего же…
                      volatile T *ptr = new volatile T;
                      • 0
                        Интересно, а в C? :)
                      • 0
                        Если вы принципиально не верите, что в этом варианте volatile работает:
                        // Think that another thread reads this array
                        template<typename T, size_t M>
                        inline void zero_func(volatile T (&arr)[M]) {
                            for(auto &i : arr) i = 0;
                        }
                        

                        Но верите, что работает здесь:
                        volatile T *ptr = new volatile T;
                        

                        То должны верить и в такой вариант :)
                        // Think that another thread reads this array
                        template<typename T, size_t M>
                        inline void zero_func(T (&arr)[M]) {
                            for(auto &i : arr) new (&i) volatile T(0);
                        }
                         
                        template<typename T, size_t M>
                        inline void zero_func(T *ptr) {
                            zero_func(reinterpret_cast<T (&)[M]>(*ptr));
                        }
                        

                        Но все же интересно, как же в C по вашему в куче иметь volatile данные? :)
                        • 0
                          Этот путь ведет в никуда — вам не удастся «убедить» меня, что в Стандарте есть что-то, что там явно не написано. Да, «вроде бы» «по логике вещей» «должно быть вот так», но тем не менее — «ссылка на явное утверждение в Стандарте, или этого не было».
                          • 0
                            Это верно. Но вопрос про C в силе :)
                            Учитывая:
                            In general, the semantics of volatile are intended to be the same in C++ as they are in C.

      • 0
        К слову, ещё есть:
        int volatile * volatile ptr_v_v;
        и это не одно и тоже, что и
        int volatile * ptr_v;

        int volatile * volatile ptr_v_v; — применяется квалификатор volatile и к адресу в котором хранится указатель, и к адресу на который указывает этот указатель.

        #include<iostream>
        #include<type_traits>
         
        int main() {
            int volatile * volatile ptr_v_v;
            int volatile * ptr_v;    
            std::cout << std::boolalpha;
            std::cout << std::is_same<decltype(ptr_v_v), decltype(ptr_v)>::value << std::endl; 
            
            return 0;
        }
        

        flase

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

Самое читаемое Разработка