Является ли вызов функции эффективным барьером памяти для современных платформ?


в кодовой базе, которую я рассмотрел, я нашел следующую идиому.

void notify(struct actor_t act) {
    write(act.pipe, "M", 1);
}
// thread A sending data to thread B
void send(byte *data) {
    global.data = data;
    notify(threadB);
}
// in thread B event loop
read(this.sock, &cmd, 1);
switch (cmd) {
    case 'M': use_data(global.data);break;
    ...
}
4 62

4 ответа:

барьеры памяти предназначены не только для предотвращения переупорядочения инструкций. Даже если инструкции не переупорядочены, это все равно может вызвать проблемы с когерентностью кэша. Что касается порядка - это зависит от вашего компилятора и настроек. ICC особенно агрессивен с переупорядочением. MSVC ж / вся оптимизация программы может быть, тоже.

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

(Если бы у меня остались голоса, я бы +1 Ваш вопрос для повествования.)

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

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

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

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

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

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

на практике, вы должны написать очень плохой код, чтобы упасть это.

на практике он прав, и в этом конкретном случае подразумевается барьер памяти.

но дело в том, что если его наличие "спорно", то код уже слишком сложен и неясен.

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

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