4

我正在阅读Vulkan Memory Allocation - Memory Host并且似乎 VkAllocationCallbacks 可以使用天真的 malloc/realloc/free 函数来实现。

typedef struct VkAllocationCallbacks {
   void*                                   pUserData;
   PFN_vkAllocationFunction                pfnAllocation;
   PFN_vkReallocationFunction              pfnReallocation;
   PFN_vkFreeFunction                      pfnFree;
   PFN_vkInternalAllocationNotification    pfnInternalAllocation;
   PFN_vkInternalFreeNotification          pfnInternalFree;
} VkAllocationCallbacks;

但我只看到两个可能的原因来实现我自己的 vkAllocationCallback:

  • 通过 Vulkan API 记录和跟踪内存使用情况;
  • 实现一种堆内存管理,它是一大块内存,可以反复使用和重用。显然,这可能是一种矫枉过正的做法,并且会遇到与托管内存相同的问题(如在 Java JVM 中)。

我在这里错过了什么吗?什么样的应用程序值得实现 vkAllocationCallbacks ?

4

3 回答 3

7

来自规范:“由于大多数内存分配不在关键路径上,这并不意味着作为一种性能特征。相反,这对于某些嵌入式系统可能很有用,用于调试目的(例如,在所有主机分配之后放置一个保护页),或用于内存分配日志记录。”

对于嵌入式系统,您可能在开始时就已经占用了所有内存,因此您不希望驱动程序调用 malloc,因为坦克中可能没有任何东西。保护页面和内存日志(仅用于调试版本)可能对谨慎/好奇的人有用。

我在某处的幻灯片上读到(不记得在哪里,抱歉),您绝对不应该实现只提供给 malloc/realloc/free 的分配回调,因为您通常可以假设驱动程序做得比这要好得多(例如,将小额分配合并到池中)。

我认为,如果您不确定是否应该实现分配回调,那么您不需要实现分配回调,也不必担心也许您应该拥有。

我认为它们适用于那些特定的用例以及那些真正想要控制一切的人。

于 2016-04-29T19:39:59.727 回答
4

这个答案试图澄清和纠正其他答案中的一些信息......

无论您做什么,都不要将 malloc/free/realloc 用于 Vulkan 分配器。Vulkan 可以并且可能确实使用对齐的内存副本来移动内存。使用未对齐的分配会导致内存损坏并且会发生坏事。腐败也可能不会以明显的方式表现出来。而是使用 posixaligned_alloc/aligned_free/aligned_realloc。它们可以在大多数系统的“malloc.h”中找到。(在windows下使用_aligned_alloc等)函数aligned_realloc不太为人所知,但它确实存在(并且已经存在多年)。顺便说一句,我的测试卡的分配器到处都有对齐请求。

将特定于应用程序的分配器传递给 Vulkan 的一件不明显的事情是,至少有一些 Vulkan 对象“记住”了分配器。例如,我将分配器传递给 vkcreateinstance 函数,并且在分配其他对象时看到来自分配器的消息非常惊讶(我也为分配器传递了一个 nullptr)。当我停下来思考时,这是有道理的,因为与 vulkan 实例交互的对象可能会导致实例进行额外的分配。

这一切都影响了 Vulkan 的性能,因为可以编写单个分配器并将其调整为特定的分配任务。这可能会影响进程启动时间。但更重要的是,将实例分配(例如,彼此靠近)放置的“块”分配器可能会对整体性能产生影响,因为它们可以提高缓存的一致性。(而不是将分配分散在内存中)我意识到这种性能“增强”是非常推测性的,但仔细调整的应用程序可能会产生影响。(更不用说 Vulkan 中许多其他值得关注的性能关键路径。)

无论您做什么,都不要尝试将aligned_alloc 类函数用作“释放”分配器,因为与Vulkan 的内置分配器(在我的测试卡上)相比,它们的性能非常差。即使在简单的程序中,与 Vulkan 的分配器相比,也存在非常显着的性能差异。(抱歉,我没有收集任何时间信息,但我绝不会反复坐下来度过那些漫长的启动时间。)

在调试方面,即使是像普通的旧 printf 这样简单的东西也可以在分配器中得到启发。添加简单统计的采集也很容易。但预计会有严重的性能损失。它们也可以用作调试钩子,而无需编写花哨的调试分配器或添加另一个调试层。

顺便说一句...我的测试卡是使用发布驱动程序的 nvidia

于 2018-06-14T22:01:38.070 回答
2

我使用普通 C 的 malloc()/realloc()/free() 实现了我自己的 VkAllocatorCallback。这是一个幼稚的实现,完全忽略了对齐参数。考虑到 64 位操作系统中的 malloc 总是返回具有 16(!)字节对齐的指针,这是相当大的对齐,这在我的测试中不会成为问题。请参阅参考资料

为了信息的完整性,16 字节对齐也是 8/4/2 字节对齐。

我的代码如下:

  /**
   * PFN_vkAllocationFunction implementation
   */
  void*  allocationFunction(void* pUserData, size_t  size,  size_t  alignment, VkSystemAllocationScope allocationScope){

    printf("pAllocator's allocationFunction: <%s>, size: %u, alignment: %u, allocationScope: %d",
        (USER_TYPE)pUserData, size, alignment, allocationScope);
   // the allocation itself - ignore alignment, for while
   void* ptr = malloc(size);//_aligned_malloc(size, alignment);
   memset(ptr, 0, size);
   printf(", return ptr* : 0x%p \n", ptr);
   return ptr;  
}

/**
 * The PFN_vkFreeFunction implementation
 */
void freeFunction(void*   pUserData, void*   pMemory){
    printf("pAllocator's freeFunction: <%s> ptr: 0x%p\n",
    (USER_TYPE)pUserData, pMemory);
    // now, the free operation !    
    free(pMemory);
 }

/**
 * The PFN_vkReallocationFunction implementation
 */
void* reallocationFunction(void*   pUserData,   void*   pOriginal,  size_t  size, size_t  alignment,  VkSystemAllocationScope allocationScope){
    printf("pAllocator's REallocationFunction: <%s>, size %u, alignment %u, allocationScope %d \n",
    (USER_TYPE)pUserData, size, alignment, allocationScope);       
    return realloc(pOriginal, size);
 }

/**
 * PFN_vkInternalAllocationNotification implementation
 */
void internalAllocationNotification(void*   pUserData,  size_t  size,   VkInternalAllocationType allocationType, VkSystemAllocationScope                     allocationScope){
  printf("pAllocator's internalAllocationNotification: <%s>, size %uz, alignment %uz, allocationType %uz, allocationScope %s \n",
    (USER_TYPE)pUserData, 
    size, 
    allocationType, 
    allocationScope);

}

/**
 * PFN_vkInternalFreeNotification implementation
 **/
void internalFreeNotification(void*   pUserData, size_t  size,  VkInternalAllocationType  allocationType, VkSystemAllocationScope                     allocationScope){
    printf("pAllocator's internalFreeNotification: <%s>, size %uz, alignment %uz, allocationType %d, allocationScope %s \n",
            (USER_TYPE)pUserData, size, allocationType, allocationScope);
}



 /**
  * Create Pallocator
  * @param info - String for tracking Allocator usage
  */
static VkAllocationCallbacks* createPAllocator(const char* info){
    VkAllocationCallbacks* m_allocator =     (VkAllocationCallbacks*)malloc(sizeof(VkAllocationCallbacks));
    memset(m_allocator, 0, sizeof(VkAllocationCallbacks));
    m_allocator->pUserData = (void*)info;
    m_allocator->pfnAllocation = (PFN_vkAllocationFunction)(&allocationFunction);
    m_allocator->pfnReallocation = (PFN_vkReallocationFunction)(&reallocationFunction);
    m_allocator->pfnFree = (PFN_vkFreeFunction)&freeFunction;
    m_allocator->pfnInternalAllocation = (PFN_vkInternalAllocationNotification)&internalAllocationNotification;
    m_allocator->pfnInternalFree = (PFN_vkInternalFreeNotification)&internalFreeNotification;
   // storePAllocator(m_allocator);
   return m_allocator;
  }

`

我使用来自 VulkanSDK 的 Cube.c 示例来测试我的代码和假设。修改版本可在此处获取GitHub

输出示例:

pAllocator's allocationFunction: <Device>, size: 800, alignment: 8, allocationScope: 1, return ptr* : 0x00000000061ECE40 
pAllocator's allocationFunction: <RenderPass>, size: 128, alignment: 8, allocationScope: 1, return ptr* : 0x000000000623FAB0 
pAllocator's allocationFunction: <ShaderModule>, size: 96, alignment: 8, allocationScope: 1, return ptr* : 0x00000000061F2C30 
pAllocator's allocationFunction: <ShaderModule>, size: 96, alignment: 8, allocationScope: 1, return ptr* : 0x00000000061F8790 
pAllocator's allocationFunction: <PipelineCache>, size: 152, alignment: 8, allocationScope: 1, return ptr* : 0x00000000061F2590 
pAllocator's allocationFunction: <Device>, size: 424, alignment: 8, allocationScope: 1, return ptr* : 0x00000000061F8EB0 
pAllocator's freeFunction: <ShaderModule> ptr: 0x00000000061F8790
pAllocator's freeFunction: <ShaderModule> ptr: 0x00000000061F2C30
pAllocator's allocationFunction: <Device>, size: 3448, alignment: 8, allocationScope: 1, return ptr* : 0x000000000624D260 
pAllocator's allocationFunction: <Device>, size: 3448, alignment: 8, allocationScope: 1, return ptr* : 0x0000000006249A80 

结论:

  • 用户实现了 PFN_vkAllocationFunction、PFN_vkReallocationFunction、PFN_vkFreeFunction 真正代表 Vulkan执行 malloc/realoc/free 操作。不确定他们是否执行所有分配,因为 Vulkan可能会自行选择分配/释放某些部分。

  • 我的实现提供的输出显示,在我的 Win 7-64/NVidia 中,请求的典型对齐是 8 个字节。这表明存在优化空间,例如托管内存,您可以在其中获取大量内存并为您的 Vulkan 应用程序(内存池)进行子分配。它可以*减少内存使用量(想想每个分配块之前的 8 个字节和之后的 8 个字节)。它也可能更快,因为 malloc() 调用可以持续更长的时间,而不是指向您自己的已分配内存池的直接指针。

  • 至少对于我当前的 Vulkan 驱动程序,PFN_vkInternalAllocationNotification 和 PFN_vkInternalFreeNotification 不会运行。可能是我的 NVidia 驱动程序中的一个错误。稍后我会检查我的 AMD。

  • *pUserData 用于调试信息和/或管理。实际上,您可以使用它来传递一个 C++ 对象,并在那里完成所有需要的性能工作。这是一种显而易见的信息,但您可以为每个调用或 VkCreateXXX 对象更改它。

  • 您可以对所有应用程序使用单个通用的 VkAllocatorCallBack 分配器,但我想使用自定义分配器可能会带来更好的结果。在我的测试中,VkSemaphore 创建显示了在小块(72 字节)的密集分配/释放上的典型模式,这可以通过在自定义分配器中重用内存上的先前块来解决。malloc()/free() 已经在可能的情况下重用了内存,但尝试使用我们自己的内存管理器很诱人,至少对于短暂的小内存块是这样。

  • 内存对齐可能是实现 VkAllocationCallback 的一个问题(没有可用的 _aligned_realoc 函数,只有 _aligned_malloc 和 _aligned_free)。但前提是 Vulkan 请求的对齐大于malloc 的默认值(x86 为 8 字节,AMD64 为 16 字节等,必须检查 ARM 默认值)。但到目前为止,至少在 64 位操作系统上,看到 Vulkan 实际上请求的内存对齐方式低于 malloc() 默认值。

最后的想法:

只需将您找到的所有 VkAllocatorCallback* pAllocator 设置为 NULL,您就可以一直快乐地生活;)可能 Vulkan 的默认分配器已经比您自己做得更好了。

但...

Vulkan 的一大亮点是开发人员可以控制一切,包括内存管理。Khronos 演示文稿,幻灯片 6

于 2016-05-01T23:59:10.810 回答