Vulkan系列教程—VMA教程—Defragmentation(碎片整理)
Posted 赵新政
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vulkan系列教程—VMA教程—Defragmentation(碎片整理)相关的知识,希望对你有一定的参考价值。
文章目录
- 前言
- 碎片整理
- 一、Defragmenting CPU memory(CPU内存碎片整理)
- 二、Defragmenting GPU memory(GPU内存碎片整理)
- 三、额外注意点
- 四、编写用户自定义内存碎片整理算法
前言
本文为Vulkan® Memory Allocator系列系列教程,定时更新,请大家关注。如果需要深入学习Vulkan的同学,可以点击课程链接,学习链接
碎片整理
长时间的穿插分配内存以及释放内存,会引起内存碎片。这种情况会引起VMA无法找到一个合适大小的空闲内存,从而导致分配失败的情况。
为了缓解这个问题,你可以使用碎片整理的功能:
Structure: VmaDefragmentationInfo2
Function: vmaDefragmentationBegin(), vmaDefragmentationEnd()
给到接口一堆Allocations,这组函数可以将已经被分配的内存进行紧凑化,保证空闲的空间都是连续的。
碎片整理做了如下事情:
- 将VmaAllocation这个内存块对象指向新的VkDeviceMemory(也会配合相应的Offset)。在整理完毕后,其 VmaAllocationInfo::deviceMemory 以及 VmaAllocationInfo::offset就会有所改变。如果你需要这方面的信息,你就可以使用 vmaGetAllocationInfo() 再把他们获取出来。
- 将实际的数据,从老的内存空间转移到新的内存空间。
碎片整理需要用户做到的:
- 重新创建VkBuffers以及VkImages,并且将他们绑定在最新的VkDeviceMemory上。你必须使用 vkDestroyBuffer(),vkDestroyImage(), vkCreateBuffer(), vkCreateImage(), vmaBindBufferMemory(), vmaBindImageMemory()这一组函数来进行上述操作。并且不要调用 vmaDestroyBuffer(), vmaDestroyImage()!!
- 重新创建与Buffers/Images绑定的Descriptors或者Views。
一、Defragmenting CPU memory(CPU内存碎片整理)
下方的案例解释了如何再CPU端进行内存整理。只有拥有HOST_VISIBLE属性的内存才能够在CPU端进行内存整理。其他的就会被忽略掉。
如何工作的:
- 当必要的时候,它Mapping(映射)了整个内存块。
- 使用memmove这个函数进行数据移动
// Given following variables already initialized:
VkDevice device;
VmaAllocator allocator;
std::vector<VkBuffer> buffers;
std::vector<VmaAllocation> allocations;
const uint32_t allocCount = (uint32_t)allocations.size();
std::vector<VkBool32> allocationsChanged(allocCount);
VmaDefragmentationInfo2 defragInfo = ;
defragInfo.allocationCount = allocCount;
defragInfo.pAllocations = allocations.data();
defragInfo.pAllocationsChanged = allocationsChanged.data();
defragInfo.maxCpuBytesToMove = VK_WHOLE_SIZE; // No limit.
defragInfo.maxCpuAllocationsToMove = UINT32_MAX; // No limit.
VmaDefragmentationContext defragCtx;
vmaDefragmentationBegin(allocator, &defragInfo, nullptr, &defragCtx);
vmaDefragmentationEnd(allocator, defragCtx);
for(uint32_t i = 0; i < allocCount; ++i)
if(allocationsChanged[i])
// Destroy buffer that is immutably bound to memory region which is no longer valid.
vkDestroyBuffer(device, buffers[i], nullptr);
// Create new buffer with same parameters.
VkBufferCreateInfo bufferInfo = ...;
vkCreateBuffer(device, &bufferInfo, nullptr, &buffers[i]);
// You can make dummy call to vkGetBufferMemoryRequirements here to silence validation layer warning.
// Bind new buffer to new memory region. Data contained in it is already moved.
VmaAllocationInfo allocInfo;
vmaGetAllocationInfo(allocator, allocations[i], &allocInfo);
vmaBindBufferMemory(allocator, allocations[i], buffers[i]);
设置 VmaDefragmentationInfo2::pAllocationsChanged这个变量是可选的,它里面会记录具体哪个Allocation被改变了。你也可以传入null,但是你随后就需要对每一个参与内存整理的Allocation使用 vmaGetAllocationInfo来获取其正确的内存信息,从而对其重新创建并且绑定。
如果你使用CustomMemoryPool,你可以填写 :
VmaDefragmentationInfo2::poolCount 以及 VmaDefragmentationInfo2::pPools
代替了:
VmaDefragmentationInfo2::allocationCount 以及VmaDefragmentationInfo2::pAllocations
这样的话是告诉VMA需要对给定的内存池整体进行碎片整理。在这种情况下,你不能使用VmaDefragmentationInfo2::pAllocationsChanged。你当然也可以有机的使用各种方法的组合。
二、Defragmenting GPU memory(GPU内存碎片整理)
对于非HOST_VISIBLE的内存们,也可以进行内存整理。为了做到这点,你需要传递一个CommandBuffer,这个cmdBuffer符合VmaDefragmentationInfo2::commandBuffer的要求。它是这么工作的:
- 它创建了临时的Buffers并且在需要的时候将他们绑定到整块内存Block上。
- 它使用了 vkCmdCopyBuffer() 来对内存内容进行拷贝
代码如下(示例):
// Given following variables already initialized:
VkDevice device;
VmaAllocator allocator;
VkCommandBuffer commandBuffer;
std::vector<VkBuffer> buffers;
std::vector<VmaAllocation> allocations;
const uint32_t allocCount = (uint32_t)allocations.size();
std::vector<VkBool32> allocationsChanged(allocCount);
VkCommandBufferBeginInfo cmdBufBeginInfo = ...;
vkBeginCommandBuffer(commandBuffer, &cmdBufBeginInfo);
VmaDefragmentationInfo2 defragInfo = ;
defragInfo.allocationCount = allocCount;
defragInfo.pAllocations = allocations.data();
defragInfo.pAllocationsChanged = allocationsChanged.data();
defragInfo.maxGpuBytesToMove = VK_WHOLE_SIZE; // Notice it is "GPU" this time.
defragInfo.maxGpuAllocationsToMove = UINT32_MAX; // Notice it is "GPU" this time.
defragInfo.commandBuffer = commandBuffer;
VmaDefragmentationContext defragCtx;
vmaDefragmentationBegin(allocator, &defragInfo, nullptr, &defragCtx);
vkEndCommandBuffer(commandBuffer);
// Submit commandBuffer.
// Wait for a fence that ensures commandBuffer execution finished.
vmaDefragmentationEnd(allocator, defragCtx);
for(uint32_t i = 0; i < allocCount; ++i)
if(allocationsChanged[i])
// Destroy buffer that is immutably bound to memory region which is no longer valid.
vkDestroyBuffer(device, buffers[i], nullptr);
// Create new buffer with same parameters.
VkBufferCreateInfo bufferInfo = ...;
vkCreateBuffer(device, &bufferInfo, nullptr, &buffers[i]);
// You can make dummy call to vkGetBufferMemoryRequirements here to silence validation layer warning.
// Bind new buffer to new memory region. Data contained in it is already moved.
VmaAllocationInfo allocInfo;
vmaGetAllocationInfo(allocator, allocations[i], &allocInfo);
vmaBindBufferMemory(allocator, allocations[i], buffers[i]);
你可以结合CPU端以及GPU端,一个操作进行完成,通过同时设置maxGpu* 与 maxCpu*。VMA当然也会自动选择最好的方法来进行每一个内存池的碎片整理。
只要你仔细规划哪些内存需要整理,哪些先使用,仔细填写vmaDefragmentationBegin()中的参数,那么你就没必要阻塞整个程序来等待,可以在一个线程里面完成。
三、额外注意点
VMA只允许对如下allocation进行整理:
- buffers
- 使用VK_IMAGE_CREATE_ALIAS_BIT, VK_IMAGE_TILING_LINEAR创建的Images,并且他们处于VK_IMAGE_LAYOUT_GENERAL 或者 VK_IMAGE_LAYOUT_PREINITIALIZED.的Layout。
如果使用VK_IMAGE_TILING_OPTIMAL这种Tiling或者其他的Layout的Image进行内存整理,就会有无法预计的后果。
如果你对Image绑定的内存进行碎片整理,那么在整理完毕之后,新创建的VkImage需要是VK_IMAGE_LAYOUT_PREINITIALIZED(保留了整理前的数据)这种格式,然后再通过ImageMemoryBarrier转换成整理前的格式。
当使用碎片整理,你可能会遇到ValidationLayer的警告,你忽略即可。
请不要期望碎片整理后,内存就会完美紧凑。VMA内部使用了一些启发式算法:
- 尽可能保证最多的空闲内存VkDeviceMemory的Blocks(每一个快是完全空白)。
- 尽可能保证每一块Block的空闲区与是连续的。
- 最小化需要被移动的已分配内存
所以内存整理完毕后,其实还会有一些碎片留存。
四、编写用户自定义内存碎片整理算法
如果你想要实现自己的内存碎片整理算法,VMA提供了一系列的基础设施功能。但是却没有从API层面上暴漏给用户。你必须自己“HACK”进它的源代码。步骤如下:
- 从VmaDefragmentationAlgorithm继承,从而派生自己的类,然后自己编写它的纯虚函数。你可以去查看其内部接口的实现以及注释。
- 你必须与VMA内部的设备描述信息打交道。如果你需要拿到更多的私有接口以及私有数据结构,那么可以使用友元类,让你的Class成为 VmaBlockMetadata_Generic的友元类。
- 如果你想要加一些自己的Flags来启用你自己的算法或者传递一些配置的Flags,你可以把他们加到VmaDefragmentationFlagBits并且再VmaDefragmentationInfo2::flags这里使用他们。
- 修改 VmaBlockVectorDefragmentationContext::Begin这个函数内容,来生成你自己的Class对象。
以上是关于Vulkan系列教程—VMA教程—Defragmentation(碎片整理)的主要内容,如果未能解决你的问题,请参考以下文章