【发布时间】:2019-04-19 19:28:36
【问题描述】:
我一直试图弄清楚如何在不调用未定义行为的情况下从 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,因为它被映射为相干内存,因此GPU可以在任何时候写入)。但是,float 数组在该内存位置技术上 不存在,至少在引用摘录的意义上不存在,因此通过这样的指针访问内存将是未定义的行为。因此,根据我的理解,我有两个选择:
1。 memcpy数据
应该始终可以使用本地缓冲区,将其转换为std::byte* 和memcpy,然后将表示 转换为映射区域。 GPU 将按照着色器中的指示解释它(在这种情况下,作为 32 位 float 的数组),从而解决了问题。但是,这需要额外的内存和额外的副本,所以我宁愿避免这种情况。
2。放置-new数组
似乎[new.delete.placement] 部分对如何获取放置地址没有任何限制(无论实现的指针安全性如何,它都不必是safely-derived pointer)。因此,应该可以通过placement-new 创建一个有效的浮点数组,如下所示:
volatile float* float_array = new (ptr) volatile float[sizeInFloats];
指针float_array 现在应该可以安全访问(在数组的范围内,或过去一次)。
所以,我的问题如下:
- 简单的
static_cast确实是未定义的行为吗? - 此展示位置-
new的用法是否已明确定义? - 这种技术是否适用于类似情况,例如accessing memory-mapped hardware?
作为旁注,我从来没有通过简单地转换返回的指针遇到问题,我只是想弄清楚这样做的 正确 方法是什么,根据标准的字母。
【问题讨论】:
-
请注意,带有数组类型的放置 new 似乎具有实现指定的内存开销。 Related question.
-
您可能对"bless" proposal 感兴趣。我想说它包括这个用例,但我承认我不是 100% 确定。
-
“我只是想弄清楚这样做的正确方法是什么” 老实说,我不明白这样做的意义。由于您的编译器无法知道
vkMapMemory是如何工作的,因此它必须假设floats 已正确创建,并且这种情况下的UB 不会产生任何后果。 -
@HolyBlackCat 这是一个危险的游戏。根据经验,它现在可能有效,但你真的会反对所有可以想象的“好像”优化,这不会变成坏事吗?
-
@HolyBlackCat:仅仅因为 C++ 规范未定义某些东西,这并不意味着编译器不能为它定义自己的某些含义——每个编译器(和每个操作系统)都扩展了规范以提供附加功能,因此您需要检查这些规范。如果 OP 使用
mmap,我会参考定义它的 POSIX 规范,但我不知道 vkMapMemory 来自哪里。大概有一个标准来定义它的作用。
标签: c++ language-lawyer c++17 volatile mapped-memory