Подтвердить что ты не робот

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

Рассмотрим этот код 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()?

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

4b9b3361

Ответ 1

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

В стандарте C99 говорится о volatile (5.1.2.3/5 "Выполнение программы" ):

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

Вот немного модифицированный пример, скомпилированный с 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