Вызывает ли функция барьер памяти? - PullRequest
20 голосов
/ 17 апреля 2011

Рассмотрим этот код C:

extern volatile int hardware_reg;

void f(const void *src, size_t len)
{
    void *dst = <something>;

    hardware_reg = 1;    
    memcpy(dst, src, len);    
    hardware_reg = 0;
}

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

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

Пожалуйста, исправьте меня, если я неправильно понимаю.

Ответы [ 5 ]

11 голосов
/ 18 апреля 2011

Компилятор не может переупорядочить операцию memcpy() до hardware_reg = 1 или после hardware_reg = 0 - это то, что volatile обеспечит - по крайней мере, настолько, насколько поток инструкций испускает компилятор. Вызов функции не обязательно является «барьером памяти», но это точка последовательности.

Стандарт C99 говорит об этом volatile (5.1.2.3/5 «Выполнение программы»):

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

Таким образом, в точке последовательности, представленной memcpy(), должен иметь место изменчивый доступ к записи 1, и не может иметь место изменчивый доступ к записи 0.

Однако есть две вещи, на которые я хотел бы указать:

  1. В зависимости от того, что означает <something>, если с буфером назначения ничего не делается, компилятор может полностью удалить операцию memcpy(). По этой причине Microsoft предложила функцию SecureZeroMemory(). SecureZeroMemory() работает на volatile квалифицированных указателях для предотвращения оптимизации записи.

  2. volatile не обязательно подразумевает барьер памяти (который является аппаратным, а не просто упорядочением кода), поэтому, если вы работаете на машине с несколькими процессорами или на определенных типах оборудования, вы может потребоваться явный вызов барьера памяти (возможно, wmb() в Linux).

    Начиная с MSVC 8 (VS 2005), Microsoft отмечает, что ключевое слово volatile подразумевает соответствующий барьер памяти, поэтому отдельный вызов специального барьера памяти может не потребоваться:

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

    • Запись в энергозависимый объект (volatile write) имеет Release семантика; ссылка на глобальный или статический объект, который возникает перед написать в энергозависимый объект в последовательность команд произойдет раньше что изменчивая запись в скомпилированном двоичная.

    • Чтение летучего объекта (volatile read) имеет семантику Acquire; ссылка на глобальный или статический объект, который возникает после чтения энергозависимая память в инструкции последовательность будет происходить после этого volatile читается в скомпилированном двоичном файле.

3 голосов
/ 17 апреля 2011

Насколько я понимаю, ваши рассуждения привели к

, компилятор не видит проблем в перемещении вызова memcpy

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

Извините, что не имею более полезной информации.

0 голосов
/ 27 декабря 2017

Вот слегка измененный пример, скомпилированный с gcc 7.2.1 на x86-64:

#include <string.h>
static int temp;
extern volatile int hardware_reg;
int foo (int x)
{
    hardware_reg = 0;
    memcpy(&temp, &x, sizeof(int));
    hardware_reg = 1;
    return temp;
}

gcc знает, что memcpy() - это то же самое, что и присвоение, и знает, что temp не доступен нигде, поэтому temp и memcpy() полностью исчезают из сгенерированного кода:

foo:
    movl    $0, hardware_reg(%rip)
    movl    %edi, %eax
    movl    $1, hardware_reg(%rip)
    ret
0 голосов
/ 17 апреля 2011

Это, вероятно, будет оптимизировано, либо потому, что компилятор вставляет вызов mecpy и исключает первое присваивание, либо потому, что он компилируется в код RISC или машинный код и там оптимизируется.

0 голосов
/ 17 апреля 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...