我一直在尝试如何在不调用未定义行为的情况下从C++17访问映射缓冲区。对于本例,我将使用Vulkan的vkMapMemory
返回的缓冲区。
因此,根据N4659(C++17的最终工作草案),[介绍对象]部分(增加了重点):
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);
(volatile
被包括在内,因为它被映射为相干存储器,因此可以在任何点由GPU写入)。然而,从技术上讲,float
数组在该内存位置中不存在,至少在引用摘录的意义上不存在,因此通过这样的指针访问内存将是未定义的行为。因此,根据我的理解,我只有两个选择:
1.memcpy
数据
应该始终可以使用本地缓冲区,将其强制转换为std::byte*
和memcpy
,表示转换为映射区域。GPU将按照着色器中的指示对其进行解释(在本例中,解释为32位float
的数组),从而解决问题。然而,这需要额外的内存和副本,所以我宁愿避免这种情况。
2.放置-new
阵列
似乎第[new.delete.placement]节没有对如何获得放置地址施加任何限制(无论实现的指针安全性如何,它都不需要是安全派生的指针)。因此,应该可以通过放置new
创建一个有效的浮点数组,如下所示:
volatile float* float_array = new (ptr) volatile float[sizeInFloats];
指针float_array
现在应该是可以安全访问的(在数组的边界内,或者过去一次)。
所以,我的问题如下:
- 简单的
static_cast
确实是未定义的行为吗 - 这个位置-
new
的用法定义明确吗 - 这种技术是否适用于类似的情况,例如访问内存映射的硬件
顺便说一句,我从来没有遇到过简单地投射返回指针的问题,我只是想根据标准的字母来找出正确的方法。
简短回答
根据标准,所有涉及硬件映射内存的东西都是未定义的行为,因为抽象机器不存在这个概念。您应该参考您的实施手册。
长答案
尽管硬件映射内存在标准中是未定义的行为,但我们可以想象任何合理的实现,只要提供一些遵循公共规则的实现。然后,一些构造比其他构造更为未定义的行为(无论这意味着什么)。
简单的
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);
这在ptr
所指向的内存中连续地构造N
未初始化的volatile float
对象。这意味着您必须在读取它们之前初始化它们;读取未初始化的对象是未定义的行为。
这种技术适用于类似的情况吗,例如访问内存映射的硬件
不,再次这实际上是实现定义的。我们只能假设您的实现采取了合理的选择,但您应该检查其文档中的内容。
C++规范没有映射内存的概念,因此就C++规范而言,与之相关的一切都是未定义的行为。因此,您需要查看正在使用的特定实现(编译器和操作系统),以了解定义了什么以及可以安全地执行什么。
在大多数系统上,映射将返回来自其他地方的内存,并且可能(也可能没有)以与特定类型兼容的方式进行了初始化。通常,如果内存最初是以正确的、受支持的形式的float
值编写的,那么您可以安全地将指针强制转换为float *
并以这种方式访问它。但您确实需要知道映射的内存最初是如何编写的。
C++与C兼容,而处理原始内存正是C的完美选择。所以别担心,C++完全有能力做你想做的事情。
- 编辑:-点击此链接获得C/C++兼容性的简单答案。-
在您的示例中,您根本不需要调用new!为了解释。。。
并非C++中的所有对象都需要构造。这些类型被称为PoD(普通旧数据)类型。它们是
1) 基本类型(浮点/整数/枚举等)
2)所有指针,但不是智能指针。3) PoD类型的阵列
4)仅包含基本类型或其他PoD类型的结构
5)类也可以是PoD类型,但惯例是,任何声明为"类"的东西都不应该被依赖为PoD。
您可以使用标准函数库对象来测试一个类型是否为PoD。
现在,关于将指针投射到PoD类型,唯一未定义的是,结构的内容不是由任何东西设置的,所以应该将它们视为"只写"值。在您的情况下,您可能已从"设备"写入它们,因此初始化它们将破坏这些值。(顺便说一句,正确的演员阵容是"relpret_cast")
您担心对齐问题是正确的,但认为这是C++代码可以解决的问题是错误的。对齐是内存的特性,而不是语言特性。要对齐内存,必须确保"偏移"始终是结构的"对齐"的倍数。在x64/x86上,错误地执行此操作不会产生任何问题,只会降低对内存的访问速度。在其他系统上,它可能会导致致命的异常
另一方面,您的内存不是"易失性"的,它是由另一个线程访问的。这个线程可能在另一个设备上,但它是另一个线程。您需要使用线程安全内存。在C++中,这是由原子变量提供的。然而,"原子"不是PoD对象!你应该使用内存围栏。这些基元强制从内存中读取内存和将内存读取到内存中。volatile关键字也可以这样做,但允许编译器对volatile写入进行重新排序,这可能会导致意外的结果。
最后,如果您希望您的代码是"现代C++"风格,您应该执行以下操作
1)声明您的自定义PoD结构以表示您的数据布局。您可以使用static_assert(std::is_pod<MyType>::value)。如果结构不兼容,则会发出警告
2)声明一个指向您的类型的指针。(仅在这种情况下,不要使用智能指针,除非有一种方法可以"释放"内存(这是有意义的)
3)只通过返回此指针类型的调用来分配内存。此函数需要
a)使用对Vulkan API的调用结果初始化指针类型
b)在指针上使用就地新建-如果只写入数据,则不需要这样做,但这是一种很好的做法。如果要设置默认值,请在结构声明中初始化它们。如果你想保留这些值,只需不给它们默认值,就地新建就不会有任何作用。
在读取内存之前使用"获取"围栏,在写入内存之后使用"释放"围栏。Vulcan可能提供了一个具体的机制,我不知道。不过,对于所有同步原语(如互斥锁/解锁)来说,暗示内存围栏是正常的,因此您可能不需要执行此步骤。