游戏引擎堆内存篇-windows

在12月份的时候写的这篇文章,感觉有很多地方都没有写到位,现重构这篇文章。(2023/2/2重构)

堆内存特征

1.堆管理器调拨的物理存储器始终是从页交换文件中分配的。

2.在默认情况下对堆的访问是依次进行的,使多个线程可以从同一个堆中分配和释放内存块。

3.一个进程可以有多块堆内存

堆申请的内存在页交换文件使得堆的效率不高,堆的依次访问特性,使得一次只能有一个线程能对堆进行操作。我实验过用malloc和heapcreate进行内存申请,ram的占用非常稳定,并没有增加。

进程中的默认堆

进程会默认开辟1m的默认堆,这个默认堆会用于ansi程序调用unicode程序,调用字节码不同函数时,对字符串进行转换时使用。

当我们使用new创建对象或malloc进行内存申请时,会使用默认堆吗?盲猜不会,后面可以把进程的所有空间内存占用情况都打印出来,可能可以找出,可能会有另一个堆。

内存申请底层

复制代码
// malloc内部实现
extern "C" _CRT_HYBRIDPATCHABLE __declspec(noinline) _CRTRESTRICT void* __cdecl malloc(size_t const size)
{
    return _malloc_dbg(size, _NORMAL_BLOCK, nullptr, 0); //申请堆空间
}

// new 内部实现
void* __CRTDECL operator new(size_t const size)
{
    for (;;)
    {
        if (void* const block = malloc(size))    //申请堆空间
        {
            return block;
        }
        if (_callnewh(size) == 0)
        {
            if (size == SIZE_MAX)
            {
                __scrt_throw_std_bad_array_new_length();
            }
            else
            {
                __scrt_throw_std_bad_alloc();
            }
        }

    }
}
复制代码

堆的底层是一个双向链表,单项_CrtMemBlockHeader定义了前指针pBlockHeaderPrev和后指针pBlockHeaderNext,通过这两个指针可以遍历程序中申请的所有堆空间。成员lRequest记录了当前堆是第几次申请的,例如第10次申请堆操作对应的数值为0x0A。成员gap为保存堆数据的数组,在Debug版下,这个数据的前后4个字节被初始化为0xFD,用于检测堆数据访问过程中是否有越界访问。

复制代码
struct _CrtMemBlockHeader
{
    _CrtMemBlockHeader* _block_header_next; //下一块堆空间首地址(实际上指向的是前一次申请的堆信息)

_CrtMemBlockHeader* _block_header_prev;     //上一块堆空间首地址(实际上指向的是后一次申请的堆信息)
    char const*         _file_name;
    int                 _line_number;

    int                 _block_use;
    size_t              _data_size;         //堆空间数据大小

    long                _request_number;    //堆申请次数
    unsigned char       _gap[no_mans_land_size];          //上溢标志

    // Followed by:
    // unsigned char    _data[_data_size]; //用户操作的堆数据
    // unsigned char    _another_gap[no_mans_land_size];        //下溢标志
};
复制代码

以上结构定义在VS SDK安装目录下的“ucrt\heap\debug_heap.cpp”文件中。

堆内存自定义申请

 HANDLE temp= HeapCreate(HEAP_NO_SERIALIZE,0,0); //申请堆内存
 LPVOID t=HeapAlloc(temp, 0,size);               //从堆中分配内存
 
 HeapFree(temp,0,t);//释放堆内存
 HeapDestroy(temp);//销毁堆

cpu高速缓存行

当cpu向内存中读取一个字节的时候,并不是只从内存中取一个字节,而是取回一个高速内存行,高速内存行可能是32/64/128字节,取决于cpu的字节对齐边界。

当一个字节被cpu1读取到内存行并修改时,如果有一个cpu2同时读取数据,那么cpu2高速内存行的值将不是最新的值。当一个cpu修改了高速缓存行的一个字节时,机器的其他cpu会收到通知,并使自己的高速内存行作废。

优化方案

1.数据与缓存行边界对齐

2.只读数据或不经常读的数据集与可读可写的数据分别存放

3.把差不多会在同一时间被访问的数据组织在一起

4.硬性关联

硬性关联

使线程在上一次运行的处理器上运行,让进程始终在同一个处理器上运行由卒于重用仍在处理器高速缓存中的数据。把若干cpu逻辑核分为若干个系统板(由多个cpu逻辑核心和内存组成),让进程在同一个系统板上运行性能将达到最佳。

SetProcessAffinityMask约束进程在掩码代表的cpu上运行。

TBB内存申请器

TBB提供了两个内存申请器scalable_aligned_malloc(内存对齐)和scalable_allocator。

scalable_allocator 解决了分配竞争的情况,它并没有完全解决高速缓存丢弃问题,cache_aligned_allocator 解决了分配竞争和缓存丢弃问题。

由于分配的内存是缓存大小的倍数所以要花费更多的空间,尤其是分配大量小空间时,会造成内存碎片化严重,可以使用内存池优化这个问题。

TBB内存加速使用:

    class SYSTEM_API MemoryWin64 :public MemoryBase
    {
    public:
        virtual void* Allocate(USIZE_TYPE uiSize, USIZE_TYPE uiAlignment, bool bIsArray);
        virtual void Deallocate(char* pcAddr, USIZE_TYPE uiAlignment, bool bIsArray);
    };
#if !_DEBUG
#if defined(_DEBUG)
#undef _DEBUG
#endif
#include <scalable_allocator.h>
void* MemoryWin64::Allocate(USIZE_TYPE uiSize, USIZE_TYPE uiAlignment, bool bIsArray)
{
	if (uiAlignment != 0)
	{
		uiAlignment = Max(uiSize >= 16 ? (USIZE_TYPE)16 : (USIZE_TYPE)8, uiAlignment);
		return scalable_aligned_malloc(uiSize, uiAlignment);
	}
	else
	{
		return scalable_malloc(uiSize);
	}
}

void MemoryWin64::Deallocate(char* pcAddr, USIZE_TYPE uiAlignment, bool bIsArray)
{
	if (!pcAddr)
	{
		return;
	}
	if (uiAlignment != 0)
	{
		scalable_aligned_free(pcAddr);
	}
	else
	{
		scalable_free(pcAddr);
	}
}

这一块的源码,我在Ue5引擎的源码里也看到了,这是和ue5同款内存申请库。

 

posted @   过往云烟吧  阅读(125)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示