punteros puntero memoria ejemplos dinamica declaracion cadenas aritmetica c++ language-lawyer c++17 volatile mapped-memory

puntero - Cómo acceder correctamente a la memoria asignada sin comportamiento indefinido en C++



punteros c++ pdf (3)

He estado tratando de averiguar cómo acceder a un búfer asignado desde C ++ 17 sin invocar un comportamiento indefinido. Para este ejemplo, vkMapMemory un búfer devuelto por vkMapMemory de Vulkan.

Entonces, de acuerdo con N4659 (el borrador de trabajo final de C ++ 17), sección [intro.object] (énfasis agregado):

Las construcciones en un programa de C ++ crean, destruyen, hacen referencia, acceden y manipulan objetos. Un objeto es creado por una definición (6.1), por una nueva expresión (8.3.4), cuando se cambia implícitamente el miembro activo de una unión (12.3), o cuando se crea un objeto temporal (7.4, 15.2).

Estas son, aparentemente, las únicas formas válidas de crear un objeto C ++. Así que digamos que obtenemos un puntero void* a una región asignada de la memoria del dispositivo visible del host (y coherente) (asumiendo, por supuesto, que todos los argumentos necesarios tienen valores válidos y la llamada se realiza correctamente, y el bloque de memoria devuelto es de Tamaño suficiente y correctamente alineado ):

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

Ahora, deseo acceder a esta memoria como una matriz float . Lo más obvio sería static_cast el puntero y continuar de la siguiente manera:

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

(La volatile se incluye ya que se asigna como memoria coherente y, por lo tanto, la GPU puede escribirla en cualquier momento). Sin embargo, técnicamente no existe una matriz float en esa ubicación de memoria, al menos no en el sentido del extracto citado, y por lo tanto, el acceso a la memoria a través de dicho puntero sería un comportamiento indefinido. Por lo tanto, según mi entendimiento, me quedan dos opciones:

1. memcpy los datos

Siempre debería ser posible utilizar un búfer local, convertirlo en std::byte* y memcpy la representación en la región asignada. La GPU lo interpretará como se indica en los sombreadores (en este caso, como una matriz de float de 32 bits) y, por lo tanto, se solucionará el problema. Sin embargo, esto requiere memoria adicional y copias adicionales, por lo que preferiría evitar esto.

2. Colocación- new la matriz

Parece que la sección [new.delete.placement] no impone ninguna restricción sobre cómo se obtiene la dirección de ubicación (no es necesario que sea un puntero derivado de forma segura, independientemente de la seguridad del puntero de la implementación). Por lo tanto, debería ser posible crear una matriz flotante válida a través de la ubicación new como sigue:

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

El puntero float_array ahora debe ser seguro para acceder (dentro de los límites de la matriz, o un pasado).

Entonces, mis preguntas son las siguientes:

  1. ¿Es el simple static_cast comportamiento indefinido de hecho?
  2. ¿Es esta ubicación - new uso bien definido?
  3. ¿Es esta técnica aplicable a situaciones similares, como el acceso a hardware mapeado en memoria ?

Como nota al margen, nunca tuve un problema simplemente al colocar el puntero devuelto, solo estoy tratando de averiguar cuál sería la forma correcta de hacerlo, de acuerdo con la letra del estándar.


Respuesta corta

Según el Estándar, todo lo que involucre memoria asignada por hardware es un comportamiento indefinido ya que ese concepto no existe para la máquina abstracta. Debe consultar su manual de implementación.

Respuesta larga

Aunque la memoria asignada por hardware no es un comportamiento definido por el Estándar, podemos imaginar cualquier implementación sensata que proporcione algunas reglas comunes que obedezcan. Algunas construcciones son entonces un comportamiento más indefinido que otras (lo que sea que eso signifique).

¿Es el simple static_cast comportamiento indefinido de hecho?

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

Sí, este es un comportamiento indefinido y se ha discutido muchas veces en .

¿Está esta nueva ubicación de uso bien definida?

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

No, aunque esto parezca bien definido, esto depende de la implementación . A medida que sucede, operator ::new[] puede reservar algunos gastos generales 1, 2 , y no puede saber cuánto, a menos que verifique la documentación de su cadena de herramientas. Como consecuencia, ::new (dst) T[N] requiere una cantidad desconocida de memoria mayor o igual a N*sizeof T y cualquier dst que asigne puede ser demasiado pequeña, lo que implica un desbordamiento del búfer.

¿Cómo proceder, entonces?

Una solución sería construir manualmente una secuencia de flotadores:

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

O equivalentemente, confiando en la Biblioteca Estándar:

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

Esto construye contiguamente N objetos volatile float no inicializados en la memoria apuntada por ptr . Esto significa que debes inicializarlos antes de leerlos; Leer un objeto sin inicializar es un comportamiento indefinido.

¿Es esta técnica aplicable a situaciones similares, como el acceso a hardware mapeado en memoria?

No, de nuevo, esto está realmente definido por la implementación . Solo podemos asumir que su implementación tomó decisiones razonables, pero debe verificar lo que dice la documentación.


C ++ es compatible con C, y manipular la memoria en bruto es algo para lo que C era perfecto. Así que no te preocupes, C ++ es perfectamente capaz de hacer lo que quieras.

  • Edición: siga este link para obtener una respuesta simple para la compatibilidad con C / C ++. -

¡En su ejemplo, no necesita llamar a nuevos en absoluto! Para explicar...

No todos los objetos en C ++ requieren construcción. Estos son conocidos como tipos PoD (datos antiguos). Son

1) Tipos básicos (floats / ints / enums, etc).
2) Todos los punteros, pero no los punteros inteligentes. 3) Arreglos de tipos de PoD.
4) Estructuras que contienen solo tipos básicos u otros tipos de PoD.
...
5) Una clase también puede ser de tipo PoD, pero la convención es que nunca se debe confiar en que cualquier cosa declarada "clase" sea PoD.

Puede probar si un tipo es PoD utilizando un PoD biblioteca de funciones estándar.

Ahora, lo único que no está definido acerca de convertir un puntero a un tipo de PoD, es que el contenido de la estructura no está establecido por nada, por lo que debe tratarlos como valores de "solo escritura". En su caso, puede haberles escrito desde el "dispositivo" y, por lo tanto, inicializarlos destruirá esos valores. (El elenco correcto es un "reinterpret_cast" por cierto)

Tiene razón en preocuparse por los problemas de alineación, pero se equivoca al pensar que esto es algo que el código de C ++ puede solucionar. La alineación es una propiedad de la memoria, no una característica del idioma. Para alinear la memoria, debe asegurarse de que el "desplazamiento" sea siempre un múltiplo de las "alignas" de su estructura. En x64 / x86, hacer esto mal no creará ningún problema, solo ralentizará el acceso a su memoria. En otros sistemas puede causar una excepción fatal.
Por otro lado, su memoria no es "volátil", es accedida por otro hilo. Este hilo puede estar en otro dispositivo, pero es otro hilo. Es necesario utilizar memoria segura para subprocesos. En C ++, esto es proporcionado por variables atomic . Sin embargo, un "atómico" no es un objeto PoD! Deberías usar una valla de memoria en su lugar. Estas primitivas obligan a leer y guardar la memoria. La palabra clave volátil hace esto también, pero el compilador puede reordenar las escrituras volátiles y esto puede causar resultados inesperados.

Finalmente, si desea que su código sea del estilo "C ++ moderno", debe hacer lo siguiente.
1) Declare su estructura PoD personalizada para representar su diseño de datos. Puede usar static_assert (std :: is_pod <MyType> :: value). Esto le avisará si la estructura no es compatible.
2) Declara un puntero a su tipo. (Solo en este caso, no use un puntero inteligente, a menos que haya una manera de "liberar" la memoria que tenga sentido)
3) Solo asigne memoria a través de una llamada que devuelva este tipo de puntero. Esta función necesita
a) Inicialice su tipo de puntero con el resultado de su llamada a la API de Vulkan.
b) Use un nuevo lugar en el puntero, esto no es necesario si solo escribe en los datos, pero es una buena práctica. Si desea valores predeterminados, inicialícelos en su declaration estructura. Si desea mantener los valores, simplemente no les dé valores predeterminados, y el nuevo en el lugar no hará nada.

Use una cerca de "adquisición" antes de leer la memoria, una cerca de "liberación" después de escribir. Vulcan puede proporcionar un mecanismo específico para esto, no lo sé. Sin embargo, es normal que todas las primitivas de sincronización (como bloqueo / desbloqueo mutex) impliquen una valla de memoria, por lo que puede escapar sin este paso.


La especificación de C ++ no tiene un concepto de memoria asignada, por lo que todo lo relacionado con ella es un comportamiento indefinido en lo que respecta a la especificación de C ++. Por lo tanto, debe consultar la implementación particular (compilador y sistema operativo) que está utilizando para ver qué se define y qué puede hacer de manera segura.

En la mayoría de los sistemas, la asignación devolverá la memoria que vino de otra parte, y puede (o no) haber sido inicializada de una manera que sea compatible con algún tipo específico. En general, si la memoria se escribió originalmente como valores float de la forma correcta y compatible, entonces puede lanzar el puntero a un float * y acceder a él de esa manera. Pero sí necesita saber cómo se escribió originalmente la memoria que se está asignando.