最近开始做UE4项目了,仰慕UnrealEngine已久,但是没有机会深入了解。这次终于有机会了解一下,所以打算好好读一读UE4的源码。又因为现在项目内存性能捉急,所以我打算从内存分配器入手。看源码是懂得越多看到的越多,我个人水平有限,还不能完完全全了解透彻。主要是给一些新手朋友们揭开神秘的源码面纱,让源码不再神秘,面对源码不再犯怵。

UE4的内存分配器是划分在硬件抽象层,即HAL(Hardware Abstraction Layer)中的。装箱内存分配器的代码具体在VS项目目录:UE4/Source/Runtime/Core/Private/HAL/MallocBinned

我们先从ApplePlatformMemory::BaseAllocator开始,从这段代码可以看出来,Mac平台的默认分配器是MallocBinned,iOS的默认分配器是MallocAnsi,我们从MallocBinned开始看。先看FMallocBinned::Malloc,再看FMallocBinned::FMallocBinned

一、确定对齐方式

FScopeLock是一个局部线程锁,当前线程离开大括号后,其他线程才能走进大括号。实际上是约定内存分配是线程同步的。

再看这段代码,首先确定下Alignment是个什么情况。我们全局搜一下,发现大家调用FMemory::Malloc的时候一般只传一个参数,Alignment使用默认的。我们看了头文件,发现没有定义Alignment的默认值,但是这个Malloc函数重载过。所以去FMallocBinned的父类FMalloc看看,可以发现Alignment的默认值是0。所以可以看出来这段代码的意思是,如果用户没有定义内存对齐,那么就使用默认的内存对齐Private::DEFAULTBINNEDALLOCATOR_ALIGNMENT

这个默认对齐又是什么呢,我们跟过去看看。

这里我们可以看到,DEFAULT_BINNED_ALLOCATOR_ALIGNMENT就是FFreeMem的大小。(以下讨论只考虑32位寻址空间)sizeof(FFreeMem)的大小我们可以确定,一个指针的大小是4bytes,uint32是4bytes,所以sizeof(FFreeMem) = 8,所以默认的内存对齐是8字节。为什么是FFreeMem?我们看下FFreeMem的定义和注释就知道,FFreeMem包含了一片内存的基本信息,每片内存的头都是它,所以一片内存最小也是8字节。如果内存对齐设小了,连FFreeMem都放不下,更别说内容了。

最后我们把这段代码翻译一下就是:如果用户没有自定义内存对齐,就使用默认的8字节内存对齐,否则使用自定义内存对齐。

二、确定有足够空间来内存对齐

这段代码,前面两行就是确定有足够空间来内存对齐。后面三行是统计用的,略过。这里我们首先要确认下SpareBytesCount是个什么,它代表了什么含义。我们假设是32位寻址空间,那么DEFAULT_BINNED_ALLOCATOR_ALIGNMENT = 8,PoolTable[0].BlockSize = 8所以

  • Size <= 8,SpareBytesCount = Size

    • Max(8, Size + (Alignment - Size)) = Max(8, Alignment) = Alignment
  • Size > 8,SpareBytesCount = 8
    • Max(8, Size + (Alignment - 8))

所以这一段代码翻译一下:分配内存大小小于8字节,按Alignment大小来匹配箱体。如果分配内存大于8字节,按Size + Alignment再减去内存头(即sizeof(FFreeMem))后的大小匹配箱体。真正Alloc的时候加上内存头,总大小Size + Alignment。为什么是这个大小呢,这个大小保证了最后Align的时候一定有足够的空间。

三、确定箱体大小

我们继续往下看代码,会发现根据Size的大小有三种不同的处理方式。我们从上到下,先看第一种情况。

我们首先确认下BinnedSizeLimit是多少,搜索“BinnedSizeLimit = ”可以发现,只有一处赋值。

其中PAGE_SIZE_LIMIT等于64k,BinnedSizeLimit为32k。所以内存分配大小为32k以下的,使用第一种方式分配(这里剧透一下,也就是装箱分配)。继续看下一行,用Size做索引,从PoolTable里取一个Table出来。那么问题来了,这个MemSizeToPoolTable是什么东西。我们稍微看一下上下文,可以发现MemSizeToPoolTable是在构造函数里初始化的。

从835行开始是MemSizeTpPoolTable的初始化,但是看这段代码前必须要理解一下PoolTable是个啥,所以我们先看看上面的代码。通过代码、注释、上下文,可以看出来PoolTable就是指32k以下的内存分为42个8字节、16字节、32字节、48字节等等大小的池子。PoolTable里面每个Table的BlockSize和MinRequest都是基本参数,疑问1:FirstPool和ExhaustedPool是干嘛用的呢,我们暂时还看不出来,带着疑问继续往下看。

知道了PoolTable的含义后,可以看出来下面这段代码意思其实是[0-8]使用第一个池子,即BlockSize为8;[9-16]使用第二个池子;[17-32]使用第三个池子;以此类推。由于MemSizeToPoolTable用的时候只会用小于32768的值来取值,所以32768和32769用来存PagePoolTable[0]、[1]。疑问2:PagePoolTable是什么呢?后面揭晓

所以总结一下,这些代码确定了箱体大小。MemSizeToPoolTable“人”如其名,就是通过Size的大小,来找到合适他的装箱内存池。至于为什么要这么麻烦还构造一个数组,应该是为了性能,这样这个查找的过程不会浪费性能。

四、初始化内存池

我们回到Malloc函数,继续往下看,现在我们取到了一个合适大小的Table,然后直接看899行,中间的代码都可以略过。

这段代码看命名可以猜测,应该是看有无FirstPool,没有的话分配一整个池子的内存,然后从内存里分配一个Block。而且这里用到了FirstPool,看来接下来能解开上面FirstPool的疑问。我们先看下AllocatePoolMemory的实现,再看看AllocateBlockFromPool的实现。

PageSize取的是Allocator.PageSize,这个值全局搜下可以发现如果物理PageSize小于64k,取64k,物理PageSize大小大于64k,取物理页大小。所以这个值一般是64k。

PoolSize是传进来的参数,Private::BINNED_ALLOC_POOL_SIZE,65536。这个值除以BlockSize,得到可以分的块数。Bytes等于这些块的内存大小,最后拿这个大小和物理页大小对齐一下。这里为什么不直接用PoolSize做对齐呢,因为有时候BlockSize会很大,这时候PoolSize - Bytes会很大,比如30k。这样用PoolSize对齐出来的OsBytes就会有30多k的浪费。总之,到348行,我们得到一个对齐后的合适的内存大小。350、351是一些断言检查,防止相关变量被魔改后产生的意外情况(这很重要:))。

353-356真正地去分配了一块内存,其中Free指向这块内存地头。358-362又是做一些检查,接下来就是初始化Pool了。

这段代码364-369和内存分配没关系,370行是给定一个内存地址,获得内存池信息。371-378的循环,如果不魔改代码是进不来的,PageSize = 64k ≈ OsBytes,其中OsBytes略小。381-382是统计接口,不用管。如果魔改了进来了呢,是什么情况?有空再来写……

如果Table->FirstPool为nullptr,这个Link就是把FirstPool指向自己。如果Table->FirstPool不为空,则将自己作为FirstPool,并且PrevLink和Next都指向原FirstPool。回答1:FirstPool是什么?所以FirstPool是尚有空余Block的Pool组成的链表。

SetAllocationSizes是初始化内存池的基本信息,后面396-397初始化了第一个Block,396记录了下剩余可用Block,397设置下一个Block为nullptr。

总结一下,这个内存池初始化,主要工作只有2点。

  1. 分配了一块和物理PageSize对齐过的内存大小
  2. 初始化了内存池的第一个Block

五、内存装箱

AllocateBlockFromPool从内存池中分配一个Block,这就是真正内存装箱的过程(把一块略小的数据放在一个标准的”Block集装箱“中)。

首先Taken自增,将已分配的块数加一。后续四行都是断言,410行可以看出FirstMem指向Pool的头,而新分配的Block则在Pool的尾部。如图:

这里为什么从Pool的尾部开始分配,而不从头部分配呢?我猜测是因为如果从头部分配,需要记录剩余Block数,而计算新Block的指针地址的时候,需要知道已分配Block数。而从尾部分配,只记录剩余Block数就行。

如果这次分配是该Pool的最后一个Block,将该Pool从链表中断开,再链接到ExhaustedPool链表上。回答1:ExhaustedPool是什么?ExhaustedPool是一个链表,保存了该BlockSize的所有已满Pool。

最后执行一下对齐,返回Block的指针。

未完待续

malloc 源码_UE4源码剖析:MallocBinned(上)相关推荐

  1. Netty 从源码的角度深入剖析 ByteBuffer

    如何从源码的角度深入剖析ByteBuffer 你只看见了调用一个方法就能创建符合要求的 ByteBuf 却不知为何如此简单, 你只看见了 Netty 使用了线程池却不知线程池用的是什么队列 你只看见了 ...

  2. 唯一插件化Replugin源码及原理深度剖析--插件的安装、加载原理

    上一篇 唯一插件化Replugin源码及原理深度剖析–唯一Hook点原理 在Replugin的初始化过程中,我将他们分成了比较重要3个模块,整体框架的初始化.hook系统ClassLoader.插件的 ...

  3. Chrome源码剖析、上--多线程模型、进程通信、进程模型

    Chrome源码剖析.上 原著:duguguiyu. 整理:July. 时间:二零一一年四月二日. 出处:http://blog.csdn.net/v_JULY_v. 说明:此Chrome源码剖析很大 ...

  4. 【chrome】Chrome源码剖析、上--多线程模型、进程通信、进程模型

     Chrome源码剖析.上 原著:duguguiyu. 整理:July. 时间:二零一一年四月二日. 出处:http://blog.csdn.net/v_JULY_v. 说明:此Chrome源码剖析很 ...

  5. 【赠书福利】掘金爆火小册同名《Spring Boot源码解读与原理剖析》正式出书了!...

    关注我们丨文末赠书 承载着作者的厚望,掘金爆火小册同名读物<Spring Boot源码解读与原理剖析>正式出书! 本书前身是掘金社区销量TOP的小册--<Spring Boot源码解 ...

  6. producer send源码_Kafka源码深度剖析系列(七)——Producer核心流程初探

    本次内容我们有两个目标:第一个初探Producer发送消息的流程第二个我们学习一下Kafka是如何构造异常体系的一.代码分析Producer核心流程初探 //因为生产中开发使用的是异步的方式发送的消息 ...

  7. 【nodejs原理源码赏析(4)】深度剖析cluster模块源码与node.js多进程(上)

    [摘要] 集群管理模块cluster浅析 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 概述 cluster模块是node.js中用于实现和管理 ...

  8. 【nodejs原理源码赏析(4)】深度剖析cluster模块源码与node.js多线程(上)

    [摘要] 集群管理模块cluster浅析 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 概述 cluster模块是node.js中用于实现和管理 ...

  9. java 头尾 队列_源码|jdk源码之栈、队列及ArrayDeque分析

    栈.队列.双端队列都是非常经典的数据结构.和链表.数组不同,这三种数据结构的抽象层次更高.它只描述了数据结构有哪些行为,而并不关心数据结构内部用何种思路.方式去组织. 本篇博文重点关注这三种数据结构在 ...

最新文章

  1. Firefox Quantum 向左,Google Chrome 向右
  2. Java入门之HelloWorld
  3. jstl java_JSTL-Java-Baby-51CTO博客
  4. 环境变量_UG环境变量设置
  5. SAP CAP 项目 cds watch 生成的 index.html 的模板位置和权限控制
  6. java 中jtable_java中使用JTable控件
  7. python正则替换查询_使用Python中的正则表达式进行搜索和替换
  8. MySQL 8.0复制性能的提升(翻译)
  9. 初学者必学教程——JQuery的简介
  10. mysql原生sql语句_原生SQL语句
  11. 微服务Eureka使用详解
  12. 盛大如何再次“盛大”
  13. python时域转频域_语音预处理(二):时域转频域
  14. 【mcuclub】温度传感器DS18B20
  15. vue项目文件命名规范推荐
  16. [OneNote同步失败记录]OneNote 当前无法同步笔记。将继续尝试。
  17. Redhat、Fedora、CentOS、OEL之间的关系与不同。
  18. oracle18c静默安装教程,Oracle 18c 19c 安装的 DBT-50000 错误解决
  19. HTML如何做一张李白的静夜思,李白《静夜思》配图赏析
  20. 写作必备文献搜索网大全

热门文章

  1. centos7.2安装五笔输入法的方法(king已测)
  2. 201803-1-跳一跳
  3. 微信小程序实质是什么? Hybrid App
  4. 【数据结构笔记46】Sort with Swap(0,*)只允许交换0的排序
  5. mysql5.5.35编译安装_CentOS 6.5最小化编译安装mysql 5.5.35
  6. python requests库详解_python爬虫之路(一)-----requests库详解
  7. verilog实现多周期处理器之——(二)第一条指令ori的实现
  8. 集算器协助MongoDB计算之交叉汇总
  9. 初级篇第三期:初识UI
  10. 基于Verilog实现呼吸灯