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。
为了使用用户自定义内存池:
- 填写VmaPoolCreateInfo 结构体。
- 调用vmaCreatePool()函数来获取VmaPool的句柄。
- 当进行分配的时候,将 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原理与实战—铸造渲染核武器—基石篇》
以上是关于Vulkan系列教程—VMA教程—用户自定义内存池的主要内容,如果未能解决你的问题,请参考以下文章
Vulkan系列教程—VMA教程—Defragmentation(碎片整理)
Vulkan系列教程—VMA教程—Defragmentation(碎片整理)