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的错误。

4.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原理与实战—铸造渲染核武器—基石篇》
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赵新政

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值