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

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

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

Конструкции в C ++ программа создает, уничтожает, обращается, получает доступ и манипулирует объектами. объект является созданный определением (6.1), новое выражение (8.3.4), когда неявно изменяется активный член union (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);

(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 ]

0 голосов
/ 17 ноября 2018

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

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

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

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

1) Базовые типы (числа с плавающей запятой / целые числа / перечисления и т. Д.).
2) Все указатели, но не умные указатели.3) Массивы типов PoD.
4) Структуры, которые содержат только базовые типы или другие типы PoD.
...
5) Класс тоже может быть PoD-типом, но соглашение состоит в том, чтовсе, что объявлено как «класс», никогда не должно полагаться на PoD.

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

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

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

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

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

0 голосов
/ 17 ноября 2018

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

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

0 голосов
/ 16 ноября 2018

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

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


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

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

Является ли простое static_cast действительно неопределенным поведением?

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

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

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

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. Это означает, что вы должны инициализировать те, прежде чем читать их; чтение неинициализированного объекта - неопределенное поведение.

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

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

...