Как правильно получить доступ к отображенной памяти без неопределенного поведения в C++

Я пытался выяснить, как получить доступ к отображенному буферу из C++17, не вызывая неопределенное поведение. Для этого примера я буду использовать буфер, возвращаемый Vulkan's vkMapMemory,

Итак, согласно N4659 (окончательный рабочий проект C++17), раздел [intro.object] (выделение добавлено):

Конструкции в программе на C++ создают, уничтожают, обращаются к объектам, обращаются к ним и управляют ими. Объект создается по определению (6.1), новым выражением (8.3.4), при неявном изменении активного члена объединения (12.3) или при создании временного объекта (7.4, 15.2).

Это, по-видимому, единственные допустимые способы создания объекта C++. Так скажем, мы получаем void* указатель на отображенную область памяти видимого хоста (и связного) устройства (конечно, при условии, что все требуемые аргументы имеют допустимые значения, и вызов завершается успешно, а возвращаемый блок памяти имеет достаточный размер и правильно выровнен):

void* ptr{};
vkMapMemory(device, memory, offset, size, flags, &ptr);
assert(ptr != nullptr);

Теперь я хочу получить доступ к этой памяти как float массив. Очевидное, что нужно сделать, это static_cast указатель и иди по моему веселому пути следующим образом:

volatile float* float_array = static_cast<volatile float*>(ptr);

(The volatile включен, поскольку он отображается как когерентная память и, таким образом, может быть записан графическим процессором в любой момент). Тем не менее, float Массив технически не существует в этой ячейке памяти, по крайней мере, не в том смысле, как в приведенном фрагменте, и, таким образом, доступ к памяти через такой указатель будет неопределенным поведением. Поэтому, насколько я понимаю, у меня есть два варианта:

1. memcpy данные

Всегда должно быть возможно использовать локальный буфер, приведите его к std::byte* а также memcpy представление в сопоставленной области. Графический процессор будет интерпретировать его как указано в шейдерах (в данном случае, как массив 32-битных float) и, таким образом, проблема решена. Однако это требует дополнительной памяти и дополнительных копий, поэтому я бы предпочел этого избежать.

2. размещение- new массив

Похоже, что раздел [new.delete.placement] не накладывает никаких ограничений на то, как получается адрес размещения (он не должен быть указателем, полученным безопасно, независимо от безопасности указателя реализации). Следовательно, должно быть возможно создать действительный массив с плавающей точкой через new следующее:

volatile float* float_array = new (ptr) volatile float[sizeInFloats];

Указатель float_array теперь должен быть безопасным для доступа (в пределах массива или в прошлом).


Итак, мои вопросы следующие:

  1. Это простой static_cast действительно неопределенное поведение?
  2. Это размещение new использование четко определено?
  3. Применима ли эта методика к подобным ситуациям, таким как доступ к оборудованию с отображенной памятью?

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

3 ответа

Решение

Короткий ответ

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


Длинный ответ

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

Это простой static_cast действительно неопределенное поведение?

volatile float* float_array = static_cast<volatile float*>(ptr);

Да, это неопределенное поведение, которое много раз обсуждалось в Stackru.

Хорошо ли определено это новое место размещения?

volatile float* float_array = new (ptr) volatile float[N];

Нет, хотя это выглядит четко определенным, это зависит от реализации. Как это происходит, operator ::new[] разрешено резервировать некоторые накладные расходы 1, 2, и вы не можете знать, сколько, если вы не проверите свою документацию по цепочке инструментов. Как следствие, ::new (dst) T[N] требует неизвестного количества памяти больше или равно N*sizeof T и любой dst Вы можете выделить слишком мало, включая переполнение буфера.

Как действовать дальше?

Решением было бы вручную построить последовательность чисел с плавающей точкой:

auto p = static_cast<volatile float*>(ptr);
for (std::size_t n = 0 ; n < N; ++n) {
    ::new (p+n) volatile float;
}

Или эквивалентно, полагаясь на Стандартную библиотеку:

#include <memory>
auto p = static_cast<volatile float*>(ptr);
std::uninitialized_default_construct(p, p+N);

Это конструирует смежно N неинициализированным volatile float объекты в памяти, на которые указывает ptr, Это означает, что вы должны инициализировать те, прежде чем читать их; чтение неинициализированного объекта - неопределенное поведение.

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

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

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

В большинстве систем отображение вернет память, которая пришла откуда-то еще, и может (или не может) быть инициализирована способом, совместимым с некоторым конкретным типом. В общем, если память изначально была написана как float значения правильной, поддерживаемой формы, то вы можете безопасно привести указатель к float * и получить к нему доступ таким образом. Но вам нужно знать, как изначально отображалась отображаемая память.

C++ совместим с C, и манипулирование необработанной памятью - то, для чего C идеально подходит. Так что не волнуйтесь, C++ вполне способен делать то, что вы хотите.

  • Изменить:- перейдите по этой ссылке для простого ответа на совместимость C/C++. -

В вашем примере вам не нужно звонить новым вообще! Объяснить...

Не все объекты в C++ требуют построения. Они известны как типы PoD (обычные старые данные). Они есть

1) Основные типы (float /ints/enums и т. Д.).
2) Все указатели, но не умные указатели. 3) Массивы типов PoD.
4) Структуры, которые содержат только базовые типы или другие типы PoD.
...
5) Класс тоже может быть PoD-типом, но соглашение заключается в том, что на все объявленное "класс" никогда не следует полагаться как на PoD.

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

Теперь единственное, что не определено в приведении указателя к PoD-типу, это то, что содержимое структуры не задается ничем, поэтому вы должны рассматривать их как значения "только для записи". В вашем случае вы, возможно, записали их с "устройства", поэтому их инициализация уничтожит эти значения. (Кстати, правильное приведение - это reinterpret_cast)

Вы вправе беспокоиться о проблемах выравнивания, но вы ошибаетесь, полагая, что это то, что может исправить код C++. Выравнивание является свойством памяти, а не языковой особенностью. Чтобы выровнять память, вы должны убедиться, что "смещение" всегда кратно "выравниванию" вашей структуры. На x64/x86 это неправильно не создаст никаких проблем, а только замедлит доступ к вашей памяти. В других системах это может привести к фатальному исключению.
С другой стороны, ваша память не является "энергозависимой", к ней обращается другой поток. Этот поток может быть на другом устройстве, но это другой поток. Вам нужно использовать поточно-ориентированную память. В C++ это обеспечивается атомарными переменными. Тем не менее, "атомный" не объект PoD! Вы должны использовать забор памяти вместо этого. Эти примитивы заставляют память считываться из памяти и в нее. Ключевое слово volatile делает то же самое, но компилятору разрешено изменять порядок изменяемых записей, что может привести к неожиданным результатам.

Наконец, если вы хотите, чтобы ваш код был в стиле "современный C++", вы должны сделать следующее.
1) Объявите вашу собственную структуру PoD, чтобы представить ваш макет данных. Вы можете использовать static_assert (std:: is_pod:: value). Это предупредит вас, если структура не совместима.
2) Объявите указатель на ваш тип. (Только в этом случае не используйте умный указатель, если нет способа "освободить" память, которая имеет смысл)
3) Распределять память только через вызов, который возвращает этот тип указателя. Эта функция должна
а) Инициализируйте ваш тип указателя с результатом вашего обращения к Vulkan API.
б) Используйте новый указатель на месте - это не требуется, если вы только записываете данные - но это хорошая практика. Если вы хотите установить значения по умолчанию, инициализируйте их в объявлении структуры. Если вы хотите сохранить значения, просто не задавайте им значения по умолчанию, и новые на месте ничего не сделают.

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

Другие вопросы по тегам