如何在C ++中正确访问映射的内存而不会发生未定义的行为

时间:2018-11-16 15:22:30

标签: c++ language-lawyer c++17 volatile mapped-memory

我一直试图弄清楚如何从C ++ 17访问映射的缓冲区而不调用未定义的行为。对于此示例,我将使用Vulkan的vkMapMemory返回的缓冲区。

因此,根据N4659(C ++ 17最终工作草案)的第[intro.object]节(着重强调):

  

C ++中的构造   程序创建,销毁,引用,访问和操作对象。一个   宾语   是   由定义(6.1),   新表达式   (8.3.4),当隐式更改a的 active成员时   联合(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现在应该可以安全地访问(在数组的边界内,或一次通过)。


所以,我的问题如下:

  1. 简单的static_cast确实是未定义的行为吗?
  2. 此展示位置-new的用法是否定义明确?
  3. 此技术是否适用于类似情况,例如accessing memory-mapped hardware

作为补充,我从来没有遇到过简单地转换返回的指针的问题,我只是想弄清楚实现该目标的正确方法是什么,标准的字母。

3 个答案:

答案 0 :(得分:8)

简短答案

根据标准,涉及硬件映射内存的所有内容均为未定义行为,因为抽象计算机不存在该概念。您应该参考实施手册。


长答案

即使标准中未定义硬件映射的内存行为,我们也可以想象任何合理的实现都可以遵循一些通用规则。这样,某些构造会比其他构造更多未定义行为(无论这意味着什么)。

  

简单的static_cast确实是未定义的行为吗?

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

是的,this is undefined behavior在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对象。这意味着您必须在读取它们之前对其进行初始化。读取未初始化的对象是未定义的行为。

  

此技术是否适用于类似情况,例如访问内存映射的硬件?

不,再次这确实是实现定义的。我们只能假设您的实现方案是合理的选择,但是您应该查看其说明文件。

答案 1 :(得分:3)

C ++规范没有映射内存的概念,因此就C ++规范而言,所有与之相关的都是不确定的行为。因此,您需要查看正在使用的特定实现(编译器和操作系统),以查看定义的内容和可以安全执行的操作。

在大多数系统上,映射将返回来自其他位置的内存,并且可能已经(也可能未)以与某些特定类型兼容的方式进行了初始化。通常,如果内存最初是用正确的受支持形式的float值编写的,则可以安全地将指针强制转换为float *并以这种方式访问​​它。但是您确实需要知道被映射的内存最初是如何写入的。

答案 2 :(得分:-2)

C ++与C兼容,并且操纵原始内存非常适合C。因此,不用担心,C ++完全有能力做您想要的事情。

  • 编辑:-请遵循此link,以获取有关C / C ++兼容性的简单答案。 -

在您的示例中,您根本不需要调用new!解释一下...

并非C ++中的所有对象都需要构造。这些称为PoD(普通数据)类型。他们是

1)基本类型(浮点数/整数/枚举等)。
2)所有指针,但不是智能指针。 3)PoD类型数组。
4)仅包含基本类型或其他PoD类型的结构。
...
5)类也可以是PoD类型,但是约定是任何声明为“ class”的东西都不应依赖PoD。

您可以使用标准函数库object测试类型是否为PoD。

现在,关于将指针转换为PoD类型的唯一定义是未定义,因为该结构的内容未设置任何内容,因此您应将它们视为“只写” ”值。在您的情况下,您可能已从“设备”向它们写入数据,因此初始化它们将破坏这些值。 (顺便说一句,正确的转换是“ reinterpret_cast”)

您担心对齐问题是正确的,但是认为这是C ++代码可以解决的问题是错误的。对齐是存储器的属性,而不是语言功能。要对齐内存,必须确保“偏移量”始终是结构的“ alignas”的倍数。在x64 / x86上,解决此问题不会造成任何问题,只会减慢对内存的访问。在其他系统上,它可能导致致命的异常。
另一方面,您的内存不是“易失性”的,它是由另一个线程访问的。该线程可能在另一个设备上,但它是另一个线程。您需要使用线程安全的内存。在C ++中,这由atomic变量提供。但是,“原子”不是PoD对象!您应该改用a memory fence。这些原语强制从内存中读取内存。 volatile关键字也可以这样做,但是允许编译器对易失性写进行重新排序,这可能会导致意外结果。

最后,如果您希望代码为“现代C ++”样式,则应执行以下操作。
1)声明您的自定义PoD结构以表示您的数据布局。您可以使用static_assert(std :: is_pod :: value)。如果结构不兼容,这会警告您。
2)声明一个指向您的类型的指针。 (仅在这种情况下,请不要使用智能指针,除非有一种方法可以“释放”有意义的内存)
3)仅通过返回此指针类型的调用分配内存。此功能需要
  a)使用对Vulkan API的调用结果来初始化指针类型。
  b)在指针上使用就地新变量-如果仅写入数据,则不需要这样做-这是一种好习惯。如果要使用默认值,请在结构declaration中对其进行初始化。如果要保留这些值,只需不为其提供默认值,就地新值将无济于事。

在读取内存之前使用“获取”防护,在写入之后使用“释放”防护。我不知道,Vulcan可能为此提供了一种特定的机制。尽管所有同步原语(例如互斥锁/解锁)都暗示有内存屏障是正常的,但如果不执行此步骤,您可能会逃脱。

相关问题