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


前言

本文为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”进它的源代码。步骤如下:

  1. 从VmaDefragmentationAlgorithm继承,从而派生自己的类,然后自己编写它的纯虚函数。你可以去查看其内部接口的实现以及注释。
  2. 你必须与VMA内部的设备描述信息打交道。如果你需要拿到更多的私有接口以及私有数据结构,那么可以使用友元类,让你的Class成为 VmaBlockMetadata_Generic的友元类。
  3. 如果你想要加一些自己的Flags来启用你自己的算法或者传递一些配置的Flags,你可以把他们加到VmaDefragmentationFlagBits并且再VmaDefragmentationInfo2::flags这里使用他们。
  4. 修改 VmaBlockVectorDefragmentationContext::Begin这个函数内容,来生成你自己的Class对象。

猜你喜欢

转载自blog.csdn.net/weixin_50523841/article/details/122461039
vma