映射GPU内存时应该使用volatile吗?

Posted

技术标签:

【中文标题】映射GPU内存时应该使用volatile吗?【英文标题】:Should volatile be used when mapping GPU memory? 【发布时间】:2021-06-09 19:50:31 【问题描述】:

OpenGL 和 Vulkan 都允许分别使用 glMapBuffervkMapMemory 来获取指向 GPU 内存的一部分的指针。他们都给映射内存一个void*。要将其内容解释为某些数据,必须将其转换为适当的类型。最简单的示例是转换为 float* 以将内存解释为浮点数或向量或类似数组。

C++ 中似乎有任何类型的内存映射is undefined behaviour,因为它没有内存映射的概念。但是,这并不是真正的问题,因为该主题超出了 C++ 标准的范围。不过还是有volatile的问题。

在链接的问题中,指针被另外标记为volatile,因为它指向的内存内容可以以编译器在编译期间无法预料的方式进行修改。这似乎是合理的,尽管我很少看到人们在这种情况下使用volatile(更广泛地说,这个关键字现在似乎几乎没有使用过)。

同时在this question 中,答案似乎是不需要使用volatile。这是因为他们所说的内存是使用mmap 映射的,后来被分配给msync,这可以被视为修改内存,这类似于在 Vulkan 或 OpenGL 中显式刷新它。恐怕这不适用于 OpenGL 和 Vulkan。

如果内存被映射为不是GL_MAP_FLUSH_EXPLICIT_BITVK_MEMORY_PROPERTY_HOST_COHERENT_BIT,则根本不需要刷新,内存内容会自动更新。即使使用vkFlushMappedMemoryRangesglFlushMappedBufferRange 手动刷新内存,这两个函数实际上都没有将映射指针作为参数,因此编译器不可能知道它们修改了映射内存的内容。

因此,是否有必要将指向映射 GPU 内存的指针标记为 volatile?我知道从技术上讲这都是未定义的行为,但我想问的是在实际硬件上实际需要什么。

顺便说一句,Vulkan Specification 或 OpenGL Specification 都没有提到 volatile 限定符。

编辑:将内存标记为volatile 会导致性能开销吗?

【问题讨论】:

这里不用担心未定义的行为,因为您不是在谈论标准中定义的抽象机器。您正在谈论特定硬件上的特定实现,并且此实现提供比标准更强大的保证。 如果文档告诉您将其标记为 volatile,那么就这样做。如果没有,那就不要。该软件的作者最了解它的工作原理以及它与硬件的交互方式,因此请听他们的意见。他们在类型中提供此信息。 vkMapMemory 在类型中是否提到volatile 如果文档没有说volatile 提供任何保证,那么它对您来说完全没用。您需要的是正确操作的保证。如果平台没有给你一个volatile,那就没有任何帮助了。你放弃了编译器优化。如果编译器支持 OpenGL 和/或 Vulkan,那么它就不会进行优化,导致符合这些标准的代码无论如何都会做错事。如果它们不受支持,那么无论哪种方式,您都处于“哎呀,我希望它有效”的领域,并且应该以某种方式离开该领域。 如果不被寻址为volatile,一个聪明的(优化的)编译器可以决定被寻址的内存区域不能(没有)被程序更新并删除它的所有读取。如果编译器可以证明程序没有读取内存区域,也可以优化写入。 【参考方案1】:

好的,假设我们有一个编译器,它对代码中发生的所有事情都无所不知。这意味着编译器可以跟踪任何指针,即使通过代码的运行时执行每次都完美而正确,无论你如何尝试隐藏它。因此,即使您在程序的一端读取了一个字节,编译器也会以某种方式记住您已读取的确切字节,并且无论何时您尝试再次读取它们,它都可以选择不执行该读取,而只为您提供先前的值,除非编译器知道有什么东西可以改变它。

但我们也可以说,我们无所不知的编译器完全忽略了 OpenGL/Vulkan 中发生的一切。对于这个编译器,图形 API 是一个黑盒子。这里有龙。

因此,您从 API 获得一个指针,从中读取,GPU 写入它,然后您想读取 GPU 刚刚写入的新数据。为什么编译器会认为该指针背后的数据已被更改?毕竟,这些更改来自系统外部,来自 C++ 标准无法识别的来源。

这就是volatile 的用途,对吧?

嗯,事情就是这样。在 OpenGL 和 Vulkan 中,为了确保您可以实际读取该数据,您需要做一些事情。即使您连贯地映射内存,您也必须进行 API 调用以确保写入内存的 GPU 进程已实际执行。对于 Vulkan,您正在等待围栏或事件。对于 OpenGL,您正在等待围栏或执行完整的完成。

无论哪种方式,在执行从内存中读取之前,无所不知的编译器都会遇到一个对黑盒的函数调用,正如之前建立的,编译器对它一无所知。由于映射指针本身来自同一个黑盒,编译器不能假定黑盒没有指向该内存的指针。因此就编译器而言,调用这些函数可能将数据写入该内存。

因此,我们无所不知的编译器无法优化掉此类内存访问。一旦我们从这些函数中获得控制权,编译器必须假设任何可通过该地址访问的指针的任何内存都可能已被更改。

如果编译器能够窥视图形 API 本身,阅读并理解这些函数在做什么,那么它肯定会看到会告诉它的东西,“哦,我不应该对通过这些指针检索的内存。”

这就是你不需要volatile的原因。

另外,请注意,这同样适用于写入数据。如果您写入持久的、连贯的映射内存,您仍然需要与图形 API 执行一些同步操作,以便您的 CPU 写入,而 GPU 不会读取它。所以这就是编译器知道它不能再依赖于它以前写入的数据的知识的地方。

【讨论】:

谢谢,我现在明白了。基本上,调用任何 Vulkan 函数都可能会修改任何内存,因为它是一个黑盒。不过有一件事让我感到困扰:在标准中,他们指出“volatile 是对实现的提示,以避免涉及对象的激进优化,因为对象的值可能会通过实现无法检测到的方式进行更改”。如果这种情况不是“无法被实现检测到”或者至少是不可预测的,那么什么时候需要使用volatile @热心_3d_graphics_pr...:直接硬件管理需要它,其中内存地址直接与某些外部设备共享任何CPU干预。 Vulkan 和 OpenGL 需要某种形式的 CPU 干预来确保数据存在。 请注意,volatile 可能有害,因为编译器无法在调用库之间删除对内存缓冲区的冗余读/写。 @motiveic_3d_graphics_pr... 问题是,如果volatile 是必要的,那你就完蛋了。您没有证据表明 volatile 就足够了。您提供了一个提示,但您如何知道该提示足以禁用正确的优化?所以如果你有其他一些足够的机制,你也不需要volatile。如果你没有其他足够的机制,你只是在希望和愿望上运作。如果您有明确说明volatile 足够的内容,您只会想要volatile

以上是关于映射GPU内存时应该使用volatile吗?的主要内容,如果未能解决你的问题,请参考以下文章

我应该同时使用 lock 和 volatile 吗?

C++ volatile关键字(多线程中声明为易变值不稳定值,告诉程序每次都从内存读取,不被编译优化,防止被优化后变量异常)

线程同步Volatile与Synchronized

Java并发编程--Volatile详解

volatile详解

需要OpenGL GPU内存清理吗?