Vulkan系列教程—VMA教程—用户自定义内存池

Posted 赵新政

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vulkan系列教程—VMA教程—用户自定义内存池相关的知识,希望对你有一定的参考价值。


前言

本文为Vulkan® Memory Allocator系列系列教程,定时更新,请大家关注。如果需要深入学习Vulkan的同学,可以点击课程链接,学习链接

Vulkan学习群:594715138

腾讯课堂《Vulkan原理与实战—铸造渲染核武器—基石篇》

网易课堂《Vulkan原理与实战—铸造渲染核武器—基石篇》


一、Custom memory pools(用户自定义内存池)

一个所谓的内存池当中,包含了一定数量的VkDeviceMemory的内存块。VMA默认情况下就已经创建并且管理了一个内存池(包含设备上的每一种类型的内存)。默认的内存池会慢慢增长,每一个内存块也都是自动被分配管理的。

你可以创建自己的内存池,如果你需要做到:

  • 将某种Allocation(分配的内存)与其他的内存区别开管理。
  • 强制内存块的大小固定。
  • 设置Vulkan分配的内存总量的最大值。
  • 保留最小或者一个固定数量的预分配的内存池大小。
  • 使用存在于 VmaPoolCreateInfo 当中,但是不存在于 VmaAllocationCreateInfo当中的额外参数。比如custom minimum alignment, custom pNext chain

为了使用用户自定义内存池:

  1. 填写VmaPoolCreateInfo 结构体。
  2. 调用vmaCreatePool()函数来获取VmaPool的句柄。
  3. 当进行分配的时候,将 VmaAllocationCreateInfo::pool 设置为自己创建的这个VmaPool句柄。而其本结构体的其他参数就不需要填写了。

示例:

// Create a pool that can have at most 2 blocks, 128 MiB each.
VmaPoolCreateInfo poolCreateInfo = ;
poolCreateInfo.memoryTypeIndex = ...//下面讲述
poolCreateInfo.blockSize = 128ull * 1024 * 1024;
poolCreateInfo.maxBlockCount = 2;
 
VmaPool pool;
vmaCreatePool(allocator, &poolCreateInfo, &pool);
 
// Allocate a buffer out of it.
VkBufferCreateInfo bufCreateInfo =  VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO ;
bufCreateInfo.size = 1024;
bufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
 
VmaAllocationCreateInfo allocCreateInfo = ;
allocCreateInfo.pool = pool;
 
VkBuffer buf;
VmaAllocation alloc;
VmaAllocationInfo allocInfo;
vmaCreateBuffer(allocator, &bufCreateInfo, &allocCreateInfo, &buf, &alloc, &allocInfo);

别忘了最后的清理工作:

vmaDestroyBuffer(allocator, buf, alloc);
vmaDestroyPool(allocator, pool);

新的VMA版本,允许使用CustomPool来创建专用内存(Dedicated Allocations)。只有在 VmaPoolCreateInfo::blockSize = 0的时候才会启用。为了使用这个特性,你需要将VmaAllocationCreateInfo::pool 设置为你的VmaPool,并且在 VmaAllocationCreateInfo::flags 上面启用VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT特性。

二、选择内存类型Index

当创建一个内存池的时候,你必须显示地规定这个内存池的内存类型Index。为了找到合适的内存类型,以便于在其中分配你的VkBuffer以及VmImage。你可以使用如下的辅助函数:vmaFindMemoryTypeIndexForBufferInfo(), vmaFindMemoryTypeIndexForImageInfo()。你需要提供一个临时的VkBuffer或者VkImage的CreateInfo,然后使用这个来调用上方函数。

示例:

VkBufferCreateInfo exampleBufCreateInfo =  VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO ;
exampleBufCreateInfo.size = 1024; // Whatever.
exampleBufCreateInfo.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; // Change if needed.
 
VmaAllocationCreateInfo allocCreateInfo = ;
allocCreateInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; // Change if needed.
 
uint32_t memTypeIndex;
vmaFindMemoryTypeIndexForBufferInfo(allocator, &exampleBufCreateInfo, &allocCreateInfo, &memTypeIndex);
 
VmaPoolCreateInfo poolCreateInfo = ;
poolCreateInfo.memoryTypeIndex = memTypeIndex;
// ...

当从Pool里面创建VkBuffer或者VkImage的时候,提供如下参数:

  • VkBufferCreateInfo:新创建的内存,应该与创建Pool使用的CreateInfo一样。如果不这样呢,你创建的内存不一定就是你需要的内存,从而引发无法预估的问题。使用不同的 VK_BUFFER_USAGE_XXX这种Flags,有可能有用,但是你最好不要从这个Pool里分配本类型内存之外种类的内存。
  • VmaAllocationCreateInfo:你不需要传入其他的参数,除了要传入使用的Pool。

三、线性内存分配算法

每一个VMA分配的内存块,都会拥有伴随而来的附属描述信息,用于描述它里面哪些被使用了,哪些是空闲的。默认来说,VMA利用描述信息以及相关算法,会在每一次分配的时候,从一堆空闲的区域种,找到最佳的分配位置。这样你可以按照任何方式分配与释放对象。


内存分配示意图

有时候你需要使用一个简单的、线性的分配算法。你可以创建一个内存池,然后将VmaPoolCreateInfo::flags设置为VMA_POOL_CREATE_LINEAR_ALGORITHM_BIT 。然后特定的线性分配算法就会被应用。这个算法总是会按照顺序,一个个的将内存块分配出去。并且,如下图所示,用完了释放的内存不会被重新使用。这个算法效率是相对较高的,并且 不需要太多的内存池描述信息占据内存。


当使用这个标识符的时候,你可以创建一个有很多用途的内存池:Free-at-once、Stack、以及环形队列。当然,不要觉得陌生,上述词语我们下面就会解释。这些选项都是自动被侦测到且执行的。

1.Free-at-once

在一个使用线性分配的内存池当中,你仍然需要手动的释放已经被你分配的内存,通过使用: vmaFreeMemory() 或者 vmaDestroyBuffer()。你可以按照任意顺序释放他们。新的内存分配总是在上一个分配出去的内存的后面一个格——中间被分配的内存,即便是释放后也不会再用到了。当你将所有从它当中分配的内存都释放掉后,内存池就空掉了,此时,分配动作又会回到内存池的开头进行。所以使用这种分配算法的情况是:如果你后面会将这些内存一口气集体释放掉。

这个模式对于 VmaPoolCreateInfo::maxBlockCount >1(有多个内存块),这个字段创建的内存池依然有效。(某些同学可能认为,多个Block怎么做呢?VMA有自己的处理方法嘛)

2.Stack

当你将目前最新分配的内存释放掉的时候,这块空间是可以被重新分配的。所以说,如果你这么使用内存:分配1号,2号,3号,然后释放3号,2号,1号。这种**LIFO(Last In First Out)**的方法,就特别适合这种策略。

这个模式对于使用了 VmaPoolCreateInfo::maxBlockCount >1(有多个内存块),这个字段创建的内存池依然有效。

3.Double stack

你分配的内存池所对应的这块内存,可能会被两个Stack使用:

  • 第一个,默认Stack,从内存的头部开始分配使用。
  • 第二个,“Upper”Stack,从内存的尾部向头部生长。

为了让本次分配使用upper Stack,你可以在VmaAllocationCreateInfo::flags上面加上 VMA_ALLOCATION_CREATE_UPPER_ADDRESS_BIT这个参数。

Double Stack这种形式,需要Pool只有一个内存块,也就是说 VmaPoolCreateInfo::maxBlockCount =1
当两个Stack生长到碰到彼此的时候,说明再也没有空间给到分配了。会抛出VK_ERROR_OUT_OF_DEVICE_MEMORY的错误。

5.Ring buffer

这样的内存策略中,会保存一个Cursor,这个Cursor会指向当前预备分配的位置,从内存头部开始,向后分配。内存的释放是按照顺序释放,遵从**FIFO(First In First Out)**原则。如果Cursor到达尾部,就会重新回到头部继续滑动(前提是头部的内存已经被释放归还了)。

Double Stack这种形式,需要Pool只有一个内存块,也就是说 VmaPoolCreateInfo::maxBlockCount =1。

三、Buddy allocation algorithm

还有一种被称为“Buddy”的内存分配算法。其内部的内存组织是按照一个二叉树进行的,每一个节点的Size都是2的幂,并且是其父亲节点的Size的一半。当你向分配一个特定Size的内存时,树就会分配一个空的Node节点。它会递归的分裂下去,直到到达你想要的内存大小,分裂的两个节点称为Buddies。但是,如果你要求的内存大小并不是2的幂,那么就得进行内存对齐,对齐到最近的2的幂上面。当两个相邻的Buddies都被释放了,就可以合并为一个大的节点。

平均上来说,Buddy Allocation是比默认的算法要快的,也会产生比较少的内存碎片。缺点就在于建立二叉树要更多的额外信息。

为了使用Buddy算法,你可以在VmaPoolCreateInfo::flags上使用 VMA_POOL_CREATE_BUDDY_ALGORITHM_BIT 这个字段。

下面列举下使用Buddy算法的限制:

  • 最好将 VmaPoolCreateInfo::blockSize 设置为2的幂。否则,比如你设置的是N,那么只能够找到一个比N小的2的幂当作blockSize了。而内存确实分配出来了N字节,则剩下的就浪费了。
  • Margins跟Corruption Detection在这个算法下就不管用了。
  • 碎片整理功能在这个算法下就不管用了。

总结

以上就是今天的内容,大家对于vulkan的学习,也可以参考我出品的vulkan系列教程,下面给大家贴出链接。

腾讯课堂《Vulkan原理与实战—铸造渲染核武器—基石篇》

网易课堂《Vulkan原理与实战—铸造渲染核武器—基石篇》

以上是关于Vulkan系列教程—VMA教程—用户自定义内存池的主要内容,如果未能解决你的问题,请参考以下文章

Vulkan系列教程—VMA教程—用户自定义内存池

Vulkan系列教程—VMA教程—Defragmentation(碎片整理)

Vulkan系列教程—VMA教程—Defragmentation(碎片整理)

Vulkan系列教程—VMA教程—Defragmentation(碎片整理)

Vulkan系列教程—VMA教程—快速上手VMA

Vulkan系列教程—VMA教程—快速上手VMA