Скользкая дорожка для поклонников volatile



Прошло уже практически 30 лет с тех пор, как в стандарте языка C появился модификатор volatile, но в наши дни это ключевое слово вызывает лишь больше вопросов и непонимания даже среди программистов, общающихся с железом на “ты”. Сейчас уже никого не удивишь многоядерным мобильным телефоном или компьютером, умещающимся в одном чипе. Прогресс не стоит на месте, компиляторы умнеют, задачи программистов усложняются, вынуждая помнить о барьерах компиляции и барьерах памяти работая на многопроцессорных системах, только volatile по-прежнему остается темным уголком стандарта, в котором лишь сказано, что доступ к такой изменчивой переменной “implementation-defined” (Стандарт C, 6.7.3/7), т.е. как решат ребята, разрабатывающие компилятор, так и будет.



Disclaimer
В данной статье я буду говорить об использовании volatile в языке С в контексте определения переменных, не касаясь ассемблерных вставок, помеченных как volatile. Так же я буду приводить примеры кода, генерируемого компилятором gcc для архитектуры x86-64, но все сказанное в полной мере относится вообще к любым современным компиляторам и архитектурам. А если ваш компилятор генерирует другой код, который работает, то это вовсе не значит, что со следующей версией поведение вашей программы, использующей volatile, не поменяется. Да и код, скорее всего, будет непереносим на другие платформы.

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

void get_and_send_password(void)
{
    char pswd[16];
    get_pass(pswd, sizeof(pswd));
    send_pass(pswd, sizeof(pswd));
    /* обнулим пароль на стеке */
    memset(pswd, 0, sizeof(pswd));
}

а вот ассемблерный код, генерируемый gcc 4.7.1 с опцией -O3:

$ gcc -S -O3 volatile.c -o -
get_and_send_password:
        subq    $24, %rsp
        movl    $16, %esi
        movq    %rsp, %rdi
        call    get_pass
        movl    $16, %esi
        movq    %rsp, %rdi
        call    send_pass
        addq    $24, %rsp
        ret

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

Ключевое слово volatile и является одним из звеньев, связывающих абстрактную машину и реальную. В USENET много постов (1, 2, 3) 20-летней давности, проливающих свет на темную историю появления этого модификатора. Приведу хороший перевод одного из писем, взятый здесь:

На уровне железа многие процессоры просто резервируют блок адресов памяти для портов ввода-вывода. Большинство процессоров имеют отдельное пространство адресов ввода-вывода, со специальными инструкциями для доступа к ним, но это не универсально (на PDP-11 такого не было, например) и даже сейчас, производители железа могут предпочесть использовать для этого адресное пространство памяти, по разным причинам. Я сомневаюсь, что кто-то так делает на архитектуре 8086 — различные адресные ограничения делают это очень сложным. Я видел это на 8080, это очень частое решение на старой TI 9900. И это был единственный способ организовать ввод-вывод на наборе инструкций PDP-11, там просто не было отдельного адресного пространства ввода-вывода (Я думаю, то же самое верно и для VAX. И не забывайте, что большинство работы на С раньше проходило именно на этих двух процессорах).

Теперь рассмотрим один из первых последовательных портов, что я использовал: Intel 8051. Нормальным способом его инициализации было записать 0 три раза в порт управления. Если у вас MMIO, то код на С мог бы выглядеть примерно так:

unsigned char* pControl = 0xff24;
*pControl = 0;
*pControl = 0;
*pControl = 0;

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

«И все заверте...»

Таким образом, volatile обязывает компилятор каждый раз обращаться к памяти, избегая возможности кеширования значения в регистрах. Для разработчиков UNIX в 80-х этот модификатор был единственным способом борьбы с компилятором, который норовил соптимизировать и выкинуть действительно нужный код.

Распространенные заблуждения при использовании volatile

1. Обращение к volatile переменной атомарны

Рассмотрим такой код:

volatile int i;
int j;

void do_inc(void)
{
	i++;
	j++;
}

что создает компилятор:

$ gcc -S -O3 volatile.c -o -
do_inc:
        movl    i(%rip), %eax
        addl    $1, j(%rip)
        addl    $1, %eax
        movl    %eax, i(%rip)
        ret

Единственная инструкция addl $1, j(%rip) для переменной j выглядит куда более «атомарнее», чем три инструкции для volatile переменной i. Выводы:

  • volatile не имеет ничего общего с атомарностью
  • компилятор создает больше инструкций при обращении к volatile переменной, что плохо сказывается на эффективности кода
  • компилятор вправе нарушить порядок инструкций. Действительно, сначала вычитывается значение i в регистр, происходит инкремент j, после чего делается инкремент старого значения i из регистра

Как бороться? Для низкоуровневых приложений: использовать атомарные инструкции или отключать прерывания на однопроцессорных системах, для программ, работающих в пространстве пользователя, использовать mutex блокировки.

2. volatile помогает создать lockless код

Это наиболее распространненное и опасное заблуждение, которое кочует из исходника в исходник. Рассмотрим код:

volatile int ready;
int buffer[100];

void do_init(int i)
{
	buffer[i] = i;
	ready = i;
}

void do_wait()
{
	while(ready)
		;
	send(buffer[ready]);
}

Программист уверен, что такое простое решение, основанное на volatile флаге, позволит ему избежать «ненужного» и «долгого» lock'а (под lock'ом я подразумеваю вызов mutex, если речь идет о userspace приложении, или запрет прерывания, если речь идет о низкоуровневом коде). А вот что создает компилятор:

$ gcc -S -O3 volatile.c -o -
do_init:
        movslq  %edi, %rax
        movl    %edi, ready(%rip)
        movl    %edi, buffer(,%rax,4)
        ret

Ожидания программиста не оправдались, вместо мифической экономии на lock'ах получилась редкая проблема, воспроизведение которой зависит от:

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

С точки зрения стандарта C данное поведение компилятора абсолютно нормально, так как он вправе переставлять volatile и не volatile конструкции. Более того, на современных архитектурах сам процессор может нарушить порядок выполнения инструкций, выполнив ready = i до инициализации буфера, но тема слабой модели упорядочивания доступа к памяти не для данной статьи.

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

asm volatile ("" ::: «memory»);

которая является барьером, говорящая компилятору «сбрось все регистры в RAM до барьера и перечитай после».

int ready;
int buffer[100];

void do_init(int i)
{
	buffer[i] = i;
	asm volatile ("" ::: "memory");
	ready = i;
}

void do_wait()
{
	while(ready)
            asm volatile ("" ::: "memory");
	send(buffer[ready]);
}

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

Кстати говоря, барьером компилятора являются вызовы функций, if/for/while конструкции, return, etc. Для детального погружения в тему смотреть Стандарт C, Annex C Sequence points, 438 p.

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

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

struct smart_struct
{
    int buffer[16];
    int done;
    int state;
};
volatile struct smart_struct s_smart_struct;

Наверное, программист надеется, что при обращении к членам данной структуры, компилятор сделает всю работу, подумав об атомарности, «правильном» порядке доступа к переменным и необходимых lock'ах. Ничего подобного не будет. На выходе будет неэффективный код, который ничего «правильного» делать не будет, так как в стандарте C ничего не сказано о том, что вообще должен делать компилятор при доступе к volatile переменной. Если код работает так, как ожидается, то это случайность. Более того, компиляторы тоже содержат баги, которые проявляются при обращении к volatile переменным, а как результат — создается неверный ассемблерный код.

Как правильно использовать volatile?

1. в контексте «asm volatile»

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

2. приведение к volatile указателю там, где нужно

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

/*
 * Пишем в физическую память для активации устройства
 */
*((volatile int*)base_addr + 0xff) = 0;
*((volatile int*)base_addr + 0xff) = 0;
*((volatile int*)base_addr + 0xff) = 0;

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

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

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

Используемые и рекомендуемые к прочтению материалы
www.kernel.org/doc/Documentation/volatile-considered-harmful.txt
thread.gmane.org/gmane.linux.kernel/526783
blog.regehr.org/archives/28
alenacpp.blogspot.ru/2006/04/volatile.html
archive.is/0PbN
gcc.gnu.org/onlinedocs/gcc/Volatiles.html



  • +14
  • 01 ноября 2012, 16:01
  • rouming

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

RSS свернуть / развернуть
Более того, компиляторы тоже содержат баги, которые проявляются при обращении к volatile переменным, а как результат — создается неверный ассемблерный код.
Наверное, «машинный код»? Это всё-таки разные вещи.
0
  • avatar
  • Alfa
  • 01 ноября 2012, 16:50
сначала препроцессор, потом ассемблер, потом линковщик -> готовый бинарь.

void foo(void)
{
	asm volatile ("blablabla");
}

что получаем:

$ gcc -c test.c -o test.o
volatile.c: Assembler messages:
volatile.c:37: Error: no such instruction: `blablabla'
0
ну а вывод _ассемблерного_ кода соответственно:

$ gcc -S test.c -o - -O3
foo:
        blablabla
        ret
0
сначала препроцессор, потом ассемблер, потом линковщик -> готовый бинарь.

Все же: препроцессор, компилятор, линкер.

Генерация промежуточного ассемблерного (именно ассемблерного) кода на этапе компиляции – вещь опциональная, хотя некоторые компиляторы всегда компилируют через asm.

Спасибо за статью.

Хочу добавить, что барьер компилятора – вещь достаточно накладная, и не стоит использовать этот механизм везде, как «серебренную пулю». Для решения задач атомарности/синхронизации во многих современных процессорах есть спец. средства (типа LDREX, STREX в Thumb-2)
0
вы путаете барьер памяти и барьер компилятора. барьер компилятора никаких дополнительных инструкций не создает. это просто указание _компилятору_ сбросить все кеши в рам. а барьер памяти, да. лочит шину памяти, требует сбросить/обновить кеш процессора. но здесь я об этом не говорил.
0
«сбросить все кеши в рам» — т.е. создать такой код, в котором побочные эффекты предыдущих инструкций до барьера не пересекались бы с побочными эффектами после. в примере из статьи это заставляет компилятор перечитать переменную снова и снова, или не нарушать порядок инструкций
0
Может я непонятно выразился, применение такого барьера заставляет компилятор генерировать код, при котором все «закешированные» переменные будут сохранны в RAM до барьера, а потом прочитаны. Но, вводя такой «запрет на кеширование» мы мешаем оптимизатору работать, генерировать оптимальный код.

Я хотел сказать, что если нам нужна атомарность/синхронность доступа к одной переменной, это можно сделать другими средствами, а не заставлять компилятор сохранять/перечитывать все переменные.
0
да конечно можно. вариантов масса. главное понимать, зачем вам это нужно, пометить в коде _по_месту_ что вы этим делаете, но не определять volatile переменную в каком-нибудь общем хедере и рассчитывать на то, что вся рутина и архитектурно зависимые вещи волшебным образом выполнится за вас.
0
В безусловно правы насчет volatile, я не предлагаю использовать volatile по причинам, которые Вы совершенно правильно описали в статье. Я говорю, например, о реализации семафора, для выполнения синхронизации потоков/прерываний.
0
в случае реализации семафора в ядре — тогда только барьер памяти и атомарные инструкции. барьер компиляции ни тем более volatile без лока шины памяти не помогут.
0
Нет, я говорю именно о барьере компилятора.

сбросить все кеши в рам

Но ведь сбросить/прочитать в RAM переменные которые «закешировались» в регистрах – это накладные расходы. Без барьера компиляции компилятор создал бы более оптимизированный код.
0
ну вы ведь хотите чтоб переменная _перечитывалась_ каждый раз.
или порядок инструкций был именно тот, который вы запланировали.
барьер компилятора именно это и сделает.
конечно, не нужно его пихать везде, где только хочется. речь именно о конкретном месте в коде, которое будет очевидно и автору кода и последователю, которому достался этот код.
0
А если я хочу перечитывать КОНКРЕТНУЮ переменную, а не всё, используемое в данной функции? Как правило перечитывать всё подряд бессмысленно.
0
используйте непосредственный каст к volatile.

/* здесь и сейчас получаю state, так как потом уже будет поздно */
int state = *(volatile int*)ptr;

switch(state) {
...
}
0
Хм… Получается что-то в стиле


int G_flags;

#define SET_FLAG(A_FLAG)  \
do{                    \
    int tmp = *(volatile int*)&G_flags;    \
    tmp |= A_FLAG;                        \
    *((volatile int*)G_flags) = tmp;        \
} while(0);
0
зачем так сложно?
*(volatile int*)&G_flags |= A_FLAG;


кстати, компилятор avr-gcc и для ваших трех строчек и для этой одной сделает одинаковый код:

foo:
	movw r30,r24
	ld r24,Z
	ldd r25,Z+1
	or r22,r24
	or r23,r25
	std Z+1,r23
	st Z,r22
	ret
 


т.е. вычитали из памяти в регистр, применили маску, снова записали регистры в память.
0
Я volatile использую, если есть структура, к которой возможно обращение из нескольких процессов в разделяемой памяти: volatile struct {...};. Так я надеюсь, что компилляторы породят ОДИНАКОВУЮ структуру для РАЗНЫХ исполняемых файлов. Это правильно или нет?.. Как-то не понял из статьи…
0
  • avatar
  • PICC
  • 01 ноября 2012, 17:26
Это правильно или нет?
конечно нет! для этого extern есть.
а волатильность — только (и только) указание компилятору, что переменная может изменяться в другом потоке, и оптимизация доступа неприменима.
0
хм. простите, я не понял о чем речь. то, о чем вы говорите, напоминает упаковку структур, если речь именно о нескольких компиляторах, а не об одном. в любом случае компилятор не порождает структуру. компилятор обращается к данным в памяти. а структура просто описывает, _как_ обращаться к этим данным (по каким смещениям и каким типам).
0
Нет, речь о другом. Допустим, В РАЗНЫХ программах — разный исходный код — используется ОДНА И ТА ЖЕ структура. Скомпилируется по-разному — может и оптимизироваться по-разному. Вот чтобы этой оптимизации не было
0
Я так понял, вы говорите о упаковке структур (выравнивании полей)? volatile на это никак не влияет, вам нужен #pragma pack.
Или я не правильно вас понял?
0
Нет:

struct A
{
    int a;
    int b;
};
receive (char *from, char *to, long size)
{
    for (long f = 0; f < size; ++f)
        *to++ = *from++;
}
main ()
{
    A str;
    receive ((char*)str, sizeof (A));
    printf ("%d", str.a);
}


Останется ли после выполнения этого кода int b в структуре?
0
Точнее, после компиляции — я же вроде бы A.b не использую
0
раз вы не обращаетесь к A.b то _кода_ не будет. но sizeof(A) конечно же будет равен двум интам (ну смотря на какой архитектуре вы находитесь, 4 (avr) или 8 байтам (x86)). вы объявили структуру. описали кусок памяти. на важно в каком месте программы. она всегда будет равна двум интам.
0
Ну, в общем, я, наверное, не прав :-)
Так что и тут volatile, скорее всего, не нужен
0
чувствуется в буквах какое-то сомнение )) да, вы вправе мне не верить. воспользуйтесь ассемблерным выводом вашего компилятора. или дизассемблером для готового бинаря. он врать (как я!) не будет.
0
Верю-верю, чес-слово :-)!
0
эм. с какой радости и куда денется часть структуры? =0
0
А ПОЧЕМУ она останется? Я же ею — структурой — полностью не пользуюсь!
0
В любом случае, компилятор не может выбросить поле из структуры, пользуетесь вы ним или нет.
0
компилятор этого не знает.
как он должен поступить, когда вы объявляете такую функцию?

void init_my_struct(struct A *a, void *data)
{
    memcpy(a, data, sizeof(a));
}
0
Понял. Теперь не буду писать там volatile Спасибо!
0
если речь действительно о разных единицах компиляции, то да, совет про extern годный:
main.cpp:
#include "my_struct.h"

extern struct my_struct s_struct;

my_struct.cpp:
#include "my_struct.h"

struct my_struct s_struct;

my_struct.h:
struct my_struct
{
...
};


_линковщик_ направит все обращения к s_struct ровно в 1 место в памяти.
0
нет. вам нужно запрещать прерывания (или спин лок или что-то еще). volatile вам никак не поможет вообще.
последовательность такая:

read s_struct.member1
===== start interrupt ======
write s_struct.member1, xxx
write s_struct.member2, xxx
===== end interrupt ======
read s_struct.member2

в итоге ваша структура неконсистентна. у вас часть данных до прерывания и часть после.
0
еще раз. если вы думаете, что «вот сюда бы здорово добавить volatile, вроде как должен помочь», то значит у вас баг. появилась мысль о volatile — баг. все просто :)
0
появилась мысль о volatile — баг

Ну, ИМХО, здесь вы слишком категоричны. В некоторых простых случаях (например, передача флага из прерывания в основной «супер-цикл» через volatile глобальную переменную) применение volatile допустимо.
0
допустимо, если выставление флага — ровно одна инструкция. а если это присвоение маски? а если со временем мигрируете на SMP и прерываний уже будет несколько от разных процессоров? да зачем такие сложности — а если вы в будущем решите использовать вложенные прерывания на AVR? ваш код с «допустимым» volatile сразу же рассыплется.
0
И каким образом тогда работать с флагами? Просто текст изобилует терминами, которые самоучкам вроде меня не очень понятны. Добавьте пример с простым объяснением на пальцах (а-ля сантехнические примеры Ди Хальта).

Я например некоторые программные регистры состояния (флаги) упаковываю в структуры битовых полей. Соответственно обозначаю эти структуры как volatile. Мне так удобно, но я не знаю, правильно это или нет. И вроде как при изменении флагов из разных прерываний никаких левых багов не возникает. При этом я конечно понимаю что атомарность volatile ни в коем разе не обеспечивает, а только убирает оптимизацию. Как быть в данном случае?
0
ну ваши флаги с volatile будут изменяться из прерывания точно так же, как и без него.
на какой архитектуре вы работаете? какой компилятор? если AVR и avr-gcc и хотите чего-то ждать (крутиться в цикле), то сделайте например так:

#define barrier() asm volatile ("" ::: "memory")

struct data
{
        int ready;
        int one;
        int two;
        int three;
};

struct data my_data;

ISR(XXX_vect)
{
        my_data.one = 0;
        my_data.two = 1;
        my_data.three = 2;
        
        /* флаг ready должен быть выставлен после обновления всей структуры. сейчас avr-gcc не переставляет
           инструкции и я _не_ использую вложенных прерывания, но кто знает, что будет завтра? */
        barrier();

        my_data.ready = 1;
}

int main(void)
{
        while (my_data.ready)
                /* ждем выставления ready из прерывания */
                barrier();

        return 0;
}


Кстати говоря, в avr-glibc даже объявлен барьер, но он не компилируется, так как пропущены "" до :::.

/usr/avr/include/avr/cpufunc.h:#define _MemoryBarrier() __asm__ __volatile__(:::«memory»)
0
Хм. Я говорю о теоретической допустимости использования volatile о «в некоторых простых случаях». Приведенный пример – как-раз такой случай. И подобный подход – супер цикл + обработчики прерываний (без вложенных прерываний) очень распространен. В этом случае, не важно, будет ли выставление флага единственной инструкцией в обработчике или нет, т. к. «супер-цикл» все равно не получит управление, пока прерывание не завершится.
В данном случае все равно, будет ли флаг выставлен в начале обработчика, в середине или в конце.

А Вы приводите примеры для SMP и nested interrupts. Я с Вами не спорю, Вы абсолютно правы, в общем случае volatile совершенно не годится для решения задач атомарности операций, синхронизации потоков и уж тем более, для межпроцессорного взаимодействия.

Но, ИМХО, в определённых случаях, использование volatile вполне допустимо.
0
да. я слишком глобально подхожу к проблеме. но просто цена _такой_ ошибки оказывается слишком велика.
0
но просто цена _такой_ ошибки оказывается слишком велика

Здесь я с Вами полностью согласен. Можно провести аналогию со злоупотреблением в использовании глобальных переменных. Это плохо, это, однозначно, плохой стиль написания кода, в многопоточных приложениях, каждая такая переменная – это потенциальный источник опасной и трудновоспроизводимой ошибки. Но вводить тотальный запрет на использование глобальных переменных (при любых обстоятельствах) – тоже нельзя (да и не реально).
0
Я на STM32 столкнулся с интересной особенностью: В некоторых функциях локальные переменные заполнялись непонятным мусором (во всяком случае в процессе отладки эти переменные показывали всякую чушь и код работал некорректно). Это случалось если функция была на 3 или 4 уровне вложенности (т.е. когда функция вызывается из другой функции, та из третьей и т.д.). После перевода локальных переменных в глобальные функция начинала работать нормально без изменения кода.
0
А переменные инициализировались?

>>После перевода локальных переменных в глобальные функция начинала работать нормально без изменения кода
Возможно стек кончился.
0
По-моему, чтобы такого не происходило, локальные переменные нужно объявлять как static. Тогда они не будут изменяться после выхода из функции.
0
Тогда они не будут изменяться после выхода из функции.
Не тот случай, или я не правильно понял…
Возможно тут о ошибке отладчика, а не о нехватке стека.
По-моему, чтобы такого не происходило, локальные переменные нужно объявлять как static
Нет, static — тоже глобальная переменная, но видимая только в пределах одной функции.
0
если вы на AVR — используйте ATOMIC_BLOCK.
avr-glibc позаботилась о вас и все нужные барьеры уже есть в этом макросе.

$ grep -r /usr/avr/include/util -e 'memory'
/usr/avr/include/util/atomic.h: __asm__ volatile ("" ::: «memory»);
/usr/avr/include/util/atomic.h: __asm__ volatile ("" ::: «memory»);
/usr/avr/include/util/atomic.h: __asm__ volatile ("" ::: «memory»);

приятно, когда ничего не нужно изобретать ))
0
2. volatile помогает создать lockless код


Интересно какие есть решения без применения непоратбельных asm() директив. Сам так делаю часто, volatile здесь очевидно не для lockless а для того чтобы цикл проверки не срезался.

Наверно монжно установку флага обернуть в функцию, но так чтобы она была static и компилер убрал call но учел, что здесь был вызов non-pure функции, так наверно не бывает. Какие ешё идеи?
0
циклом вы вводите ожидание, т.е. синхронизируете один контекст выполнения с другим, значит вам _необходима_ консистентность каких-то данных при ready == 1. это значит, что вы выполняете lock-and-wait. volatile вам не дает _никаких_ гарантий, что данные будут консистенты. компилятор вправе переставить инструкции. процессор вправе выполнять их в любом порядке. вам не гарантируется _ничего_.

любой вызов static функции скорее всего заинлайнится, и компилятор опять вправе сделать перестановку. без барьера (или нового sequence point) здесь не обойтись.
0
Я все понял, вопрос как решить проблему. Предположим, что процессор не обладает способностью переупорядочивать обращения к памяти. То есть надо только заставить компилятор выдать ту же последовательность записей, что и в исходнике.

вам не гарантируется _ничего_.

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

Очевидное решение о котором я говорю, сделать так.


void do_sync()
{
        ready = 1;
}

void do_init(int i)
{
        buffer[i] = i;
        do_sync();
}


Хотя надо ещё позаботиться о том, чтобы код do_sync не был подставлен, и функция не считалась pure. Но проблем не в этом, а в том, что происходит вызов там где он не нужен. Возможно есть какое-то более изящное решение.


asm volatile ("" ::: "memory");


Это мне не нравиться, непортабельно, и наверно может быть неоптимально в сложных случаях.

Хотя на C ничего наверно лучше не придумать, оптимально было бы явно указывать оптимизатору, что вот эту пару операций переставлять нельзя, а остальные можно.
0
Как вариант, чтобы do_sync() компилятор не считал pure-функцией, можно попробовать так


volatile int  VolatileONE = 1;
…
void do_sync()
{
        ready = VolatileONE;
}



Я не уверен в этом решении, не знаю, как поступит оптимизатор, если захочет заинлайнить данную функцию. В стандарте четкого ответа я не нашел.
0
если захочет заинлайнить данную функцию.
noinline запретили?
0
noinline — не запретили, его никогда не разрешали :)
Это ключевое слово – специфическое расширение, в стандарте оно не прописано.
0
вам шашечки, или ехать?
0
:)

Если «ехать» – то можно сделать asm–вставку и не париться. Но здесь речь идет «шашечках» — о реализации на чистом, стандартном С.
0
Мне интересно, в конструкции volatile ("" ::: «memory») есть какая-то логика? Откуда это «шеститочие»? Выглядит совсем не сишно. Или это к асму относится? Что может быть в первых кавычках? Что может быть кроме memory?
А, уже нашел. Оказывается вот. Жесть какая. Опять гланды через жопу. «И эти люди мне запрещают в носу ковыряться».
0
  • avatar
  • ACE
  • 02 ноября 2012, 00:47
Есть замечание:
memory barriers обеспечивают надлежащий порядок доступа к volatile переменным
memory barriers НЕ обеспечивают надлежащий порядок доступа к не-volatile переменным.(Может происходить перебрасывание через барьер. )
Источник (с примером ): www.nongnu.org/avr-libc/user-manual/optimization.html
0
фишка в том, что конструкция __asm__ volatile ("" ::: «memory»); не содержит никаких инструкций, поэтому компилятору переставлять/оптимизировать нечего. в статье, что вы привели, речь идет о cli/sti. а с ними все хитро получается. так как реально барьер это лишь фраза «эй, сбрось все в память до и перечитай после», поэтому этот трюк работает для выставления флагов, находящихся в памяти. cli/sti к памяти никакого отношения не имеют, поэтому с точки зрения абстрактной машины эти инструкции не создают _никаких_ побочных эффектов. а вообще, эта ссылка в очередной раз подтверждает, что все еще хуже, чем есть, а фраза
«Unfortunately, at the moment, in avr-gcc (nor in the C standard), there is no mechanism to enforce complete match of written and executed code ordering» — рвет шаблоны и заставляет всех лизать кегли.

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

кстати, у gcc c версии 4.4 есть __sync_syncrhonize, что должно быть эквивалентно __asm__ volatile ("" ::: «memory»);. хотя, я не смотрел, какой получается в итоге код.
0
ИМХО костыль какой-то. Для низкоуровневых операций лучше применить ассемблерную вставку, а не пытаться использовать неподходящий инструмент.
0
  • avatar
  • m0xf
  • 02 ноября 2012, 09:04
Подскажите пожалуйста, что означает объявление переменной, массива или структуры сразу как volatile и как static?
Спасибо.
0
>>сразу как volatile
Сразу — это как? Можно не «сразу»?
0
это значит, что
1. переменная будет видна только в той единицы трансляции (файле), где объявлена. за это отвечает модификатор static.
2. если инициализации у переменной отсутствует, то компилятор присвоит ей 0.
static int v; /* v == 0 */
за это тоже отвечает модификатор static.
3. переменная будет существовать все время жизни вашей программы. опять static.
4. каждое обращение к переменной — это всегда доступ к памяти этой переменной, т.е. не будет никакого кеша в регистрах или оптимизации. за это отвечает volatile.

кстати, даже можно писать так:
static const volatile int i;
и модификаторы const и volatile никак не противоречат друг другу, хотя, казалось бы, одновременно «изменчивый» и «постоянный» не могут ужиться вместе.
0
эм. а смысл в const volatile?
0
вы переменную не можете изменить. const.
переменная может быть изменена волшебными силами извне (общая память с устройством, например). volatile.
каждое обращение на _чтение_ переменной — лезем в память, читаем. никаких оптимизаций. volatile.
0
эм. если const, то это совсем не переменная.
а раз она магическим образом меняется — нихера она не const…
0
вас не смущает такой код?
int i = 0;
const int *p = &i;
int ii = *p; /* ii == 0 */
i = 666;
ii = *p; /* ii == 666 */
константа изменилась.
0
Обманули компилятор — i не const.
0
хм. но ведь это константный указатель, а не указатель на константу.
0
хорошо. усложниим. добавим _везде_ const
const int const * const p = &i
0
такой избыточной конструкцией вы сказали:
1. p не может быть изменен как указатель
2. данные не могут быть модифицированы через p.
ничего о реальной константности данных, на которые указыает p, не сказано
0
const int *p = &i;
p — это указатель на константу int. Проверьте в How to read.
Данный код ни чуть не смущает. Здесь запрещается смана значения переменной по указателю. Очень часто такое применяется параметрах функции, и в передаче объектов. По сути эквивалентно:
void printi(const int* pi) {printf("%d", *pi);}
int i = 0;
printi(&i);
i=666;
printi(&i);
В этом же коде ничего не смущает?
0
Aлиса, ты меня не путай! Сейчас в рожу вцеплюсь!

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

Так что никакая переменная типа const здесь не изменялась так как ее впринципе нет
0
эм. если const, то это совсем не переменная.
В этом вашем С конст — самая обычная переменная. Только компилер не дает к ней на запись обращаться.
0
вообще конечно да. но имхо это тот случай, когда если нельзя, но очень хочется, то можно. но не нужно.
0
Ну почему не нужно. Если предположить вариант, что есть некий аппаратный регистр в памяти, откуда нужно читать, но нельзя писать, то объявить его как const volatile — самое то.
0
да, в принципе согласен. но это частности.
0
Это наверное из того случая когда никто не заставляет «си»программиста писать запутанные и непонятные программы, но все возможности для этого имеются

:)
0
переменная будет видна только в той единицы трансляции (файле), где объявлена. за это отвечает модификатор static.

Позволю небольшое уточнение. static переменная может также быть объявлена внутри функции. В таком случае это будет «глобальная» переменная, область видимости которой ограничен данной функцией.

Если говорить о С++ — то объявление static переменной внутри класса создаёт «глобальную» переменную, единственный экземпляр которой будет «общим» для всех экземпляров класса.
0
в компиляторе KEIL (а может и не только) есть разновидности барьеров:
__schedule_barrier();
Просто барьер. Разграничение между предыдущими операциями и последующими. В память ничего не сохраняет.
__force_stores();
Барьер + сохранение всех измененных переменных, которые по отношению к функции — внешние. Локальные переменные функции, из которой этот барьер вызывается — не сохраняются.
__memory_changed();
Барьер + сохранение всех переменных. Глобальных и локальных.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.