游戏引擎基础组件——内存管理

2019-10-11 05:43 来源:未知
void Panda::Allocator::FreeAll() { PageHeader* pFree = m_pPageList; while  { PageHeader* pTemp = pFree; pFree = pFree->pNext; delete[] reinterpret_cast<uint8_t*>; } // 只重置这两个属性因为这两个属性是要用于判断的,其他的都是保存作用 m_pPageList = nullptr; m_pFreeBlockList = nullptr;}

1. Shallow Copy - 浅拷贝

故宫角楼是很多摄影爱好者常去的地方,夕阳余辉下的故宫角楼平静而安详。

 private: static Allocator* m_pAllocators; static uint32_t* m_pLookUpTable;

2. Byte Swapping Floating-Point Values - 字节交换浮点值

即使在单个平台上,浮点值也可以有许多不同的表示形式。 除非你非常小心,否则尝试在平台边界上传递浮点值会让人无尽的头疼。 为了帮助您处理浮点数,Core Foundation定义了一组函数和两个特殊的数据类型以及整数交换函数。 这些函数允许您对32位和64位浮点值进行编码,以便稍后对其进行解码,并在必要时进行字节交换。 Listing 3 显示了如何编码64位浮点数,Listing 4显示了如何解码它。

// Listing 3  Encoding a Floating Point Value
Float64 myFloat64;

CFSwappedFloat64 swappedFloat;

// Encode the floating-point value.

swappedFloat = CFConvertFloat64HostToSwapped(myFloat64);

// Listing 4  Decoding a floating-point value

Float64             myFloat64;
CFSwappedFloat64    swappedFloat;

// Decode the floating-point value.
myFloat64 = CFConvertFloat64SwappedToHost(swappedFloat);

数据类型CFSwappedFloat32CFSwappedFloat64在规范表示中包含浮点值。 CFSwappedFloat本身不是浮点数,不应该直接用作浮点数。 然而,您可以发送一个到另一个进程,保存到磁盘或通过网络发送。 由于格式是通过转换函数转换为规范格式的,因此不需要显式交换API。 如果需要,在格式转换过程中,会为您处理字节交换。

一 进程

PageHeader结构体比BlockHeader多出一个方法,用于获取这一页的第一个块的地址,就是PageHeader这个结构体之后的位置。具体的分配器类,对外至少需要3个接口:分配内存块、释放内存块、重置分配器。对每一个分配器类来说,它的页尺寸有多大,块尺寸有多大,对齐的方式是什么样子的,这些都是固定的,通过重置操作来设置。除此之外,因为要对齐,所以每一个块里有多少是实际能使用的尺寸,有多少是对齐补足的尺寸,这些都要有明确的记录。基于这些思考,我们可以给分配器添加这些属性:

Creating Custom Allocators - 创建自定义分配器

要创建自定义分配器,首先声明并初始化CFAllocatorContext类型的结构。 将版本字段初始化为0,并将任何所需的数据(如控制信息)分配并分配给info字段。 此结构的其他字段是在下面的Implementing Allocator Callbacks中描述的函数指针。

一旦将适当的值分配给CFAllocatorContext结构的字段,则调用CFAllocatorCreate函数来创建allocator对象。 该函数的第二个参数是指向结构的指针。 此函数的第一个参数标识用于为新对象分配内存的分配器。 如果要在CFAllocateContext结构中为此使用allocate回调,请为第一个参数指定kCFAllocatorUseContext常量。 如果要使用默认分配器,请在此参数中指定NULL

// Listing 1  Creating a custom allocator

static CFAllocatorRef myAllocator(void) {

static CFAllocatorRef allocator = NULL;

if (!allocator) {

CFAllocatorContext context =

{0, NULL, NULL, (void *)free, NULL,

myAlloc, myRealloc, myDealloc, NULL};

context.info = malloc(sizeof(int));

allocator = CFAllocatorCreate(NULL, &context);

}

return allocator;

}

1 进程和程序

进程:是一个可执行程序的实例。

程序:包含一系列信息的文件,这些信息描述了如何在运行时创建一个进程。包含如下信息:

  1. 二进制格式标识:如最常见的ELF格式。
  2. 机器语言指令:对程序算法进行编码。
  3. 程序入口地址:标识程序开始执行时的起始指令位置。
  4. 数据:程序文件包含的变量初始值和程序使用的字面常量值,如字符串。
  5. 符号表和重定位表:描述程序中函数和变量的位置及名称。
  6. 共享库和动态链接信息:程序文件中所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态链接器的路径名。
  7. 其他信息。

进程的再定义:进程是由内核定义的抽象的实体,并为该实体分配用以执行程序的各项系统资源。

从内核的角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。

相对而言,这个雏形还是非常简单的。其他的分配策略比如顺序分配,实现方式就和这篇文章里的不同了,顺序分配并不把内存分块,而是保有一个头部结构和一个尾部结构来指明当前有多少内存是可用的,需要分配的时候,分配出去的内存也需要有一个头部结构和一个尾部结构,这当然会造成浪费(本文的实现中没有尾部结构,头部结构也是作为分配内存的一部分返回的。因为我们的大小是固定的,没有再保存相关信息的必要。),但是这也更加灵活(不需要那么多分配器了)。更详细的实现方案,请参考下面列出的参考资料五。

1. Byte Swapping Integers - 字节交换整数

Core Foundation为字节交换提供了三个优化的基本功能 - CFSwapInt16CFSwapInt32CFSwapInt64。 所有其他交换函数都使用这些原语完成他们的工作。 一般来说,您不需要直接使用这些原语。

尽管原始交换功能无条件交换,但是较高级别的交换功能是以不需要字节交换的方式进行定义的,换句话说,当源和主机字节顺序相同时,它们不会执行任何操作。 对于整数类型,这些函数采用CFSwapXXXBigToHostCFSwapXXXLittleToHostCFSwapXXXHostToBigCFSwapXXXHostToLittle的格式,其中XXX是诸如Int32的数据类型。 例如,如果您在一个小端点机器上读取数据为网络字节顺序(big-endian)的网络中的16位整数值,则可以使用函数CFSwapInt16BigToHost。 Listing 1演示了这个过程。

// Listing 1  Swapping a 16 bit Integer

SInt16  bigEndian16;
SInt16  swapped16;

// Swap a 16 bit value read from network.
swapped16 = CFSwapInt16BigToHost(bigEndian16);

Byte Ordering部分介绍了一个简单的C结构示例,该C结构创建并保存到小端机上的磁盘,然后从大端机器上的磁盘读取。 为了纠正这种情况,您必须交换每个字段中的字节。 Listing 2中的代码演示了如何使用Core Foundation字节交换函数来完成此操作。

// Listing 2  Byte swapping fields in a C structure

// Byte swap the values if necessary.
aStruct.int1 = CFSwapInt32LittleToHost(aStruct.int1)
aStruct.int2 = CFSwapInt32LittleToHost(aStruct.int2)

假设一个大端的架构,Listing 2中使用的函数将交换每个字段中的字节。 Figure 1显示了字段交换对aStruct.int1字段的影响。 请注意,字节交换代码在小端机上运行时不会执行任何操作。 编译器应优化代码并保留数据不变。

图片 1

Figure 1 Four-byte little-endian to big-endian swap

首先,了解一下进程的基本概念,进程在内存中布局和内容。

此外,还需要知道运行时是如何为动态数据结构(如链表和二叉树)分配额外内存的。

最后,补足Reset函数中调用的FreeAll函数。释放所有内存需要一页一页地删除分配的内存块,直到所有的内存块都删除,并且重置相关属性:

版本记录

版本号 时间
V1.0 2017.10.07

1 在堆上分配内存

堆:一段长度可变的连续虚拟内存,始于进程的未初始化数据段末尾,随着内存的分配和释放而增减。将堆的当前内存顶部边界称为“程序中断(program break)”

program break是一个非常重要的概念,因为分配和释放内存的实际动作就是改变进程的program break位置。

program break的起始位置(堆的大小为0)位于未初始化数据段末尾之后。

细节:在分配新的内存后,program break位置升高,程序可以访问新分配区域内的任何内存地址,而此时物理内存页尚未分配。内存会在进程首次试图访问这些虚拟内存地址时自动分配新的物理内存页。

本文做了一个内存管理器类(MemoryManager)和一个分配器类(Allocator)的雏形。MemoryManager类是用户的交互接口,所有的内存分配和回收操作都是通过MemoryManager类提供的方法来执行的。Allocator类是真正保有内存块的类,它是一种分配策略的抽象,每一个Allocator实例都表示一种分配策略。MemoryManager类保存不同的Allocator来满足不同的内存分配需求,这也是MemoryManager的核心职责之一。

Core Foundation Object Lifecycle Management - Core Foundation对象生命周期管理

感兴趣的可以看一下这里

Core Foundation对象的使用寿命取决于其引用计数 - 希望对象持久存在的客户端数量的内部计数。 在Core Foundation中创建或复制对象时,其引用计数设置为1。 随后的客户端可以通过调用CFRetain来声明对象的所有权,CFRetain会增加引用计数。 后来,当你没有更多的使用对象,你调用CFRelease。 当引用计数达到0时,对象的分配器释放对象的内存。

 

在3D游戏引擎中,通常都会有一个内存管理器来控制内存的分配和回收操作。为什么呢?因为相对于CPU计算速度来说,内存与CPU之间的传输速度实在是太慢了,更可怕的是如果直接使用系统的分配内存接口,还涉及到CPU上下文的切换,这个更加慢得可怕,到底有多慢,看看下面的数字就就知道了:

Byte Swapping - 字节交换

如果您需要找到主机字节顺序,您可以使用函数CFByteOrderGetCurrent。 可能的返回值为CFByteOrderUnknownCFByteOrderLittleEndianCFByteOrderBigEndian

三 编程需要注意的事项

通过对内存相关知识更多的了解,在平时编程的时候,应更清楚为什么我们需要遵守下面的规则。

  1. 分配一块内存后,不要改变这块内存范围外的任何内容。
  2. 释放同一块已分配内存超过一次是错误的。当两次释放同一块内存时,常见的后果是导致不可预知的行为。
  3. 若非经由malloc函数包中函数所返回的指针,绝不能在调用free()函数使用。
  4. 如果需要反复分配内存,那么应当确保释放所有已使用完毕的内存,不然将导致内存泄露。

 

虽然在我们平时的工作当中,可能涉及不到这么底层的原理,但是通过对这些基本原理的了解,可以让我们更加清除,我们写代码究竟在写些什么 :)

 

参考资料:

《Linux/Unix系统编程手册(上册)》 第6章,第7章 

Panda::BlockHeader* Panda::Allocator::NextBlock(BlockHeader* pCurrentBlock) { return reinterpret_cast<BlockHeader*>( reinterpret_cast<uint8_t*> (pCurrentBlock)   m_nBlockSize);}

1. Implementing Allocator Callbacks - 实现分配器回调

CFAllocatorContext结构有七个定义回调函数的字段。 如果创建自定义分配器,则必须至少实现allocate函数。 分配器回调应该是线程安全的,如果回调函数调用其他函数,它们也应该是重入的。

保留,释放和复制描述回调都以CFA1locatorContext结构的info字段为单参数。 键入为void *,此字段指向您为分配器定义的任何数据,例如包含控制信息的结构体。

Retain 回调:

const void *(*retain)(const void *info);

info中保留您为分配器上下文定义的数据。 这只有在数据是Core Foundation对象时才有意义。 您可以将此函数指针设置为NULL

Release回调

void (*release)(const void *info);

Release(或free)您为分配器上下文定义的数据。 您可以将此函数指针设置为NULL,但这样做可能会导致内存泄漏。

Copy Description回调:

CFStringRef (*copyDescription)(const void *info);

返回对描述您的分配器的CFString的引用,特别是用户定义数据的某些特性。 您可以将此函数指针设置为NULL,在这种情况下,Core Foundation将提供基本描述。

Allocate回调

void *   (*allocate)(CFIndex size, CFOptionFlags hint, void *info);

分配至少size字节的内存块,并返回指向块开头的指针。 hint参数是一个你现在应该不使用的位域。 size参数应该始终大于0,如果不是,或者发生分配问题,返回NULL。 此回调可能不为NULL

Reallocate回调

void *   (*reallocate)(void *ptr, CFIndex newsize, CFOptionFlags hint, void *info);

将由ptr指向的内存块的大小更改为由newsize指定的大小,并将指针返回到较大的内存块。 在任何重新分配失败时返回NULL,使旧的内存块不变。 请注意,ptr参数永远不会为NULL,而newsize将始终大于0 - 除非满足这两个条件,否则不使用该回调。

将旧内存块的内容保持不变,直到较小的新尺寸或旧尺寸。 如果ptr参数不是先前由分配器分配的内存块,则结果未定义;异常程序终止可能发生。 hint参数是一个你现在应该不使用的位域。 如果将此回调设置为NULL,则当它尝试使用该分配器时,CFAllocatorReallocate函数在大多数情况下返回NULL。

Deallocate回调

void   (*deallocate)(void *ptr, void *info);

使ptr指向的内存块可用于分配器的后续重用,但不可用于程序的继续使用。 ptr参数不能为NULL,如果ptr参数不是先前由allocator分配的内存块,则结果未定义,异常程序终止可能发生。 您可以将此回调设置为NULL,在这种情况下,CFAllocatorDeallocate函数不起作用。

Preferred Size回调

CFIndex   (*preferredSize)(CFIndex size, CFOptionFlags hint, void *info);

返回分配器可能分配的实际大小,给出对size大小的内存块的请求。 hint参数是一个你现在应该不使用的位域。


2 典型的进程内存布局

图片 2

每个进程所分配的内存由很多部分组成,通常称之为“段(segment)”。如上图所示:

  1. 文本段:包含进程运行的程序机器语言指令。文本段具有只读属性,因此多个进程可同时运行同一程序,共享文本段。
  2. 初始化数据段:包含显式初始化的全局变量和静态变量。当程序加载到内存时,从可执行文件中读取这些变量的值。
  3. 未初始化数据段(BSS段,block started by symbol):包含了未进行显式初始化的全局变量和静态变量。程序启动之前,系统将本段内所有内存初始化为0.所以又叫做零初始化数据段。
  4. 栈(stack):动态增长和收缩的段,由栈帧(stack frame)组成。系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量、实参和返回值。
  5. 堆(heap):在运行时为变量动态进行内存分配的一块区域。堆顶端成为程序中断(program break)

将经过初始化的全局变量和静态变量与未经过初始化的全局变量和静态变量分开存放,其主要原因在于程序在磁盘上存储时,没有必要为未经过初始化的变量分配存储空间。相反,可执行文件只需记录未初始化数据段的位置及所需要大小,直到运行时再由程序加载器来分配这一空间。

需要注意一点时,该内存布局的讨论是在虚拟内存中的,并不是物理内存中的布局。

在后面会专门讨论虚拟内存的一些细节。

 

void Panda::Allocator::Free { BlockHeader* pHeader = reinterpret_cast<BlockHeader*>; pHeader->pNext = m_pFreeBlockList; m_pFreeBlockList = pHeader; m_FreeBlockCount  ;}

2. Releasing Object References - 释放对象引用

要减少Core Foundation对象的引用计数,请将对该对象的引用传递给CFRelease函数的参数:

CFRelease(myString);

重要提示:您不应该直接释放Core Foundation对象(例如,通过对它调用free)。 完成对象后,调用CFRelease功能,Core Foundation将正确处理它。

图片 3

struct BlockHeader { BlockHeader* pNext; // 指向下一个块头处};

3. Copying Object References - 复制对象引用

当您复制对象时,生成的对象的引用计数为1,而不考虑原始对象的引用计数。 有关复制对象的更多信息,请参阅Copy Functions。

函数malloc和free

malloc函数声明

#include
void *malloc(size_t size); 

作用:在堆上分配参数size字节大小的内存。

返回值:成功返回指向新分配内存起始地址的指针,失败返回NULL

free函数声明 

#include
void free(void *ptr);


作用:释放ptr参数所指向的内存块,该参数应该是之前由malloc或者其他内存分配函数之一所返回的地址。

需要注意的是:一般情况下,free并不降低program break的位置,而是将这块内存增加到空闲内存列表中,供后续的malloc函数循环使用。因为:

  • 被释放的内存块通常位于堆的中间,而非堆的顶部,因而降低program break是不可能的。
  • 它最大限度地减少了内核调用调整program break系统调用的次数。
  • 通常程序会持有分配的内存或者反复释放和重新分配,而不是释放所有内存再运行一段时间。

仅当堆顶空闲内存“足够”大的时候,free函数的glibc实现会调用sbrk()来降低program break的地址,至于“足够”与否则取决于malloc函数包行为的控制参数(128KB为典型值)。这减少了必须对sbrk()发起的调用次数。

分配函数用到了通过Block的指针获取下一个块地址的函数,这个函数是这样实现的:将当前的Block指针强制转换成指向uint8_t的指针,然后偏移m_BlockSize,在将得到的指针强制转成BlockHeader返回。

Using the Allocator Context - 使用分配器上下文

Core Foundation中的每个分配器都有一个上下文。 上下文是定义对象的操作环境的结构,通常由函数指针组成。 分配器的上下文由CFAllocatorContext结构定义。 除了函数指针之外,结构还包含版本号和用户定义数据的字段。

// Listing 1  The CFAllocatorContext structure

typedef struct {
    CFIndex version;
    void * info;
    const void *(*retain)(const void *info);
    void (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
    void * (*allocate)(CFIndex size, CFOptionFlags hint, void *info);
    void * (*reallocate)(void *ptr, CFIndex newsize, CFOptionFlags hint, void *info);
    void (*deallocate)(void *ptr, void *info);
    CFIndex (*preferredSize)(CFIndex size, CFOptionFlags hint, void *info);
} CFAllocatorContext;

info字段包含分配器的任何特别定义的数据。 例如,分配器可以使用info字段来跟踪特殊的分配。

重要提示:对于当前版本,不要将version字段的值设置为0以外的任何值。

如果在分配器上下文(info字段)中有一些用户定义的数据,请使用CFAllocatorGetContext函数获取分配器的CFAllocatorContext结构。 然后根据需要评估或处理数据。 以下代码提供了一个示例:

// Listing 2  Getting the allocator context and user-defined data

static int numOutstandingAllocations(CFAllocatorRef alloc) {
    CFAllocatorContext context;
    context.version = 0;
    CFAllocatorGetContext(alloc, &context);
    return (*(int *)(context.info));
}

其他Core Foundation函数调用在分配器上下文中定义的与内存相关的回调,并将一个无类型的指针取回或返回到一个内存块(void *):

  • CFAllocatorAllocate,分配一个内存块。
  • CFAllocatorReallocate,重新分配一块内存。
  • CFAllocatorDeallocate,取消分配一块内存。
  • CFAllocatorGetPreferredSizeForSize,根据给定的一个请求,给出了可能被分配的内存大小。

二 内存分配

就像前文所说的,Allocator是真正保有向操作系统申请的内存块的对象,每一种Allocator都对应着一种分配策略。思路是:将内存块分为页和块两个单位。一页内存都包含多个块,当这一页内存被用完了,就再向系统申请一页内存。页与页之间,块与块之间采用链表的方式来连接,方便分配与回收。而页本身不作为分配的单位,它只是我们为了节省时间,提前向系统申请的内存的尺寸。

4. Determining an Object's Retain Count - 确定对象的保留计数

如果您想知道Core Foundation对象的当前引用计数,则将对该对象的引用传递给CFGetRetainCount函数的参数:

CFIndex count = CFGetRetainCount(myString);

但是请注意,除了调试之外,通常不需要确定Core Foundation对象的引用计数。 如果您发现自己需要知道对象的保留计数,请检查您是否正确遵守所有权政策规则(请参阅Ownership Policy)。


malloc和free的实现

malloc()的实现

  1. 扫描之前由free()所释放的空闲内存块列表,以求找到尺寸大于或者等于要求的一块内存
  2. 如果这一内存块的尺寸正好与要求相当,就把它直接返回给调用者。
  3. 如果是一块较大的内存,那么将对其进行分割,在将一块大小相当的内存返回给调用者的同时,把较小的那块空闲内存块保留在空闲列表。
  4. 如果在空闲内存列表中找不到足够大的空闲内存块,那么malloc会调用sbrk()以分配更多的内存,并且malloc会分配出比所需字节数更多的内存,将超出的部分置于空闲内存列表中。

free()的实现

首先先了解两点:malloc返回的内存块和空闲列表中的内存块的结构

为了知道每一个内存块的大小,当malloc分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值。该整数位于内存块的起始处,而实际返回给调用者的内存地址恰好位于这一长度记录字节之后。如下图所示:

图片 4

为了管理空闲内存列表,free()会使用内存块本身的空间来存放链表指针,将自身添加到列表中。如下图所示:

图片 5

所以,在频繁地分配和释放内存之后,堆中的链表可能会变成下图的样子,空闲链表中的空闲内存会和已分配的在用内存混杂在一起。

图片 6

 

内存管理器的核心功能,分配与释放内存,有了查询表之后,分配和释放的过程变得非常简单。如果分配/释放的内存过大,那么就用原生的c方法来分配/释放。

1. Retaining Object References - 保留对象引用

要增加Core Foundation对象的引用计数,请将对该对象的引用传递给CFRetain函数的参数:

/* myString is a CFStringRef received from elsewhere */

myString = (CFStringRef)CFRetain(myString);

一、让 CPU 告诉你硬盘和网络到底有多慢二、从零开始手敲次世代引擎三、Memory Management part 2 of 3: C-Style Interface四、Memory Management五、3D游戏引擎设计

前言

Core Foundation框架(CoreFoundation.framework)是一组C语言接口,它们为iOS应用程序提供基本数据管理和服务功能。接下来我们就详细的解析这个框架。感兴趣的可以看我上面写的几篇。
1. CoreFoundation框架详细解析(一) —— 基本概览
2. CoreFoundation框架详细解析(二) —— 设计概念
3. CoreFoundation框架详细解析(三) —— 内存管理(一)

struct PageHeader { PageHeader* pNext; BlockHeader* BlockStart() { return reinterpret_cast<BlockHeader*>; // PageHeader之后就是块的起始位置 }};

Copy Functions - 复制函数

通常,当使用=运算符将一个变量的值分配给另一个变量时,会发生标准复制操作(也可能称为简单赋值)。例如,表达式myInt2 = myInt1会使myInt1的整数内容从myInt1使用的内存复制到myInt2使用的内存中。在复制操作之后,两个独立的内存区域包含相同的值。但是,如果您尝试以这种方式复制Core Foundation对象,请注意,您不会重复对象本身,仅仅是对对象的引用。

例如,Core Foundation的新用户可能会认为要创建CFString对象的副本,她将使用表达式myCFString2 = myCFString1。同样,此表达式实际上并不复制字符串数据。因为myCFString1和myCFString2都必须具有CFStringRef类型,所以此表达式仅复制对该对象的引用。复制操作后,您有两份CFString的引用。这种类型的副本非常快,因为只有引用是重复的,但重要的是要记住以这种方式复制可变对象是危险的。与使用全局变量的程序一样,如果应用程序的一部分使用引用的副本更改对象,那么程序的其他具有该引用副本的部分就无法知道数据已更改。

如果要复制对象,则必须使用Core Foundation提供的函数之一专门用于此目的。继续使用CFString示例,您将使用CFStringCreateCopy创建一个包含与原始数据相同的数据的全新CFString对象。具有CreateCopy函数的Core Foundation类型还提供了可以修改的对象的副本CreateMutableCopy的变体。

 public: // C  11的新机制,可变参数模板 template<typename T, typename... Arguments> T* New(Arguments... parameters) { return new (Allocate))T(parameters...); } template<typename T> void Delete { p->~T(); Free(reinterpret_cast<void*>, sizeof; }

Byte Ordering - 字节排序

微处理器架构通常使用两种不同的方法将多字节数字数据的各个字节存储在存储器中。这种差异被称为byte orderingendian nature。大多数情况下,您的计算机的端序格式可以被安全地忽略,但在某些情况下,它变得至关重要。 OS X提供了一种将数据的一种端形式转化为另外一种端模式的各种函数。

Intel x86处理器首先存储最低有效字节的双字节整数,后跟最高有效字节。这称为小端字节排序。其他CPU(如PowerPC CPU)首先存储其最高有效字节的双字节整数,后跟其最低有效字节。这被称为大字节字节排序。大多数时候,您的计算机的端序格式可以安全地忽略,但在某些情况下,它变得至关重要。例如,如果您尝试从与您的端点性质不同的计算机上创建的文件读取数据,则字节排序的差异可能会产生不正确的结果。从网络读取数据时也会发生同样的问题。

术语:术语big-endianlittle-endian来自Jonathan Swift的十八世纪讽刺Gulliver的旅行。 Blefuscu帝国的主体被分为两个派系:从大端开始吃蛋的人和从小端起吃蛋的人。

给出一个讨论端格式问题的具体例子,考虑一个简单的C结构的例子,它定义了两个四字节整数,如Listing 1所示。

// Listing 1  Example data structure

struct {
    UInt32 int1;
    UInt32  int2;
} aStruct;

假设Listing 2中所示的代码用于初始化Listing 1所示的结构。

// Listing 2  Initializing the example structure

ExampleStruct   aStruct;

aStruct.int1 = 0x01020304;
aStruct.int2 = 0x05060708;

考虑Figure 1中的图表,其中显示了大端处理器或内存系统如何组织示例数据。 在大端系统中,物理内存被组织,每个字节的地址从最高到最低的。

图片 7

Figure 1 Example data in big-endian format

请注意,这些字段存储在左侧的更高有效字节和右侧较少有效字节。 这意味着地址字段Int1的最高有效字节的地址是0x98,而地址0x9B对应于Int1的最低有效字节。

图2中的图表显示了一个小端系统如何组织数据。

图片 8

Figure 2 Example data in little-endian format

请注意,每个字段的最低地址现在对应于最低有效字节,而不是最高有效字节。如果要在小端系统上打印Int1的值,您将看到尽管以不同的字节顺序存储,但它仍然被正确解释为十进制值16909060。

现在假设由Listing 2所示的代码初始化的示例数据值是在小端系统上生成并保存到磁盘。假设数据以字节地址顺序写入磁盘。当通过大端系统从磁盘读取时,数据将再次布置在存储器中,如Figure 2所示。问题是数据仍然是小端字节顺序,即使它是在大型端系统。该差异导致值被错误评估。在本示例中,Int1域的十进制值应为16909060,但是由于字节排序不正确,因此它被评估为67305985.这种现象称为字节交换,一般发生在当一个端格式的数据被使用其他字符串格式时。

不幸的是,这是一般情况下无法解决的问题。原因是您交换的方式取决于数据的格式。字符串通常不会被交换,长字交换四字节到端,字交换两个字节端到端。因此,需要交换数据的任何程序必须知道数据类型,源数据端序和主机端序。

CFByteOrder.h中的函数允许您对双字节和四字节整数以及浮点值进行字节交换。适当使用这些功能可以帮助您确保程序操作的数据正确。有关使用这些函数的详细信息,请参阅Byte Swapping 部分。 请注意,Core Foundation的字节交换函数仅适用于OS X。


为了方便使用,提供一个New和一个Delete模板函数,这里可以用语法糖的方法来实现Delete函数

后记

未完,待续~~~

图片 9

我的CPU主频是3.6GHz,也就是每个指令执行的时间为0.26ns。内存每次寻址所需要的时间是100ns,是CPU执行一次指令所需要时间的385倍,一次 CPU 上下文切换需要大约 1500ns,也就是1.5us,是CPU执行一次指令所需时间的5769倍。从人类感知的角度上看,如果说CPU执行一次指令的时间为1s,那么CPU上下文切换一次大概就需要1.6小时,我都可以睡一觉了!

2. Deep Copy - 深拷贝

当您要创建一个全新的复合对象时,必须执行深层复制。 深层复制复制复合对象及其所有对象的内容。 Core Foundation的当前版本包括执行属性列表深度复制的函数(请参阅CFPropertyListCreateDeepCopy)。 如果要创建其他结构的深层副本,则可以通过递归递减到复合对象并逐个复制其所有内容来执行深层副本。 当复合对象可以递归时,请注意实现此功能 - 它们可以直接或间接包含对其自身的引用 - 这可能导致递归循环。


 static const uint32_t k_BlockSizes[] = { // 4字节增加 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, // 32字节增加 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 608, 640, // 64字节增加 704, 768, 832, 896, 960, 1024 }; static const uint32_t k_PageSize = 8192; // 页尺寸 static const uint32_t k_Alignment = 4; // 对齐值 static const uint32_t k_BlockSizeCount = sizeof (k_BlockSizes) / sizeof (k_BlockSizes[0]); // 预配置的分配器数量 static const uint32_t k_MaxBlockSize = k_BlockSizes[k_BlockSizeCount - 1]; // 预配置的分配器分配的最大内存数

Using Allocators in Creation Functions - 在创建函数中使用分配器

每个Core Foundation不透明类型都有一个或多个创建函数,该函数创建并返回以特定方式初始化的该类型的对象。所有创建函数都将其作为第一个参数作为对分配器对象(CFAllocatorRef)的引用。某些函数也可能具有用于专门分配和释放目的的分配器参数。

分配器参考参数有几个选项:

  • 你可以传递常量kCFAllocatorSystemDefault,这指定了通用系统分配器(它是初始默认分配器)。
  • 您可以传递NULL来指定当前的默认分配器(可能是自定义分配器或通用系统分配器)。这与传递kCFAllocatorDefault相同。
  • 您可以传递常量kCFAllocatorNull,该常量指示不分配的分配器,尝试使用它是错误的。一些创建函数具有用于重新分配或释放后备存储的特殊分配器的参数,通过为参数指定kCFAllocatorNull,可以防止自动重新分配或释放。
  • 您可以使用CFGetAllocator函数获得另一个Core Foundation对象使用的分配器的引用。通过使用相同的分配器分配它们,可以将相关对象放入内存zone中。
  • 您可以传递对自定义分配器的引用(请参阅Creating Custom Allocators)。

如果要使用自定义分配器,并且要使其成为默认分配器,建议首先使用CFAllocatorGetDefault函数获取对当前默认分配器的引用,并将其存储在局部变量中。完成使用自定义分配器后,使用CFAllocatorSetDefault函数将存储的分配器重置为默认分配器。


同样,页的组织方式与块类似,所以页头的结构也不能缺:

复制复合对象,可以包含其他对象的集合对象等对象也必须小心处理。正如您所期望的,使用=运算符对这些对象执行副本会导致对象引用的重复。与CFStringCFData这样的简单对象相反,为复合对象(如CFArray和CFSet)提供的CreateCopy函数实际上会执行浅拷贝。在这些对象的情况下,浅层复制意味着创建新的集合对象,但是原始集合的内容不会被复制

只有对象引用被复制到新的容器。如果您有一个不可变的数组,并且您想对其进行重新排序,则此类型的副本很有用。在这种情况下,您不想复制所有包含的对象,因为不需要更改它们,以及为什么要使用额外的内存?您只需要更改包含的对象集。与使用简单类型复制对象引用相同的风险也适用。

接下来,我们着手实现这个分配器。我们的块是用链表的结构串联起来的,所以一个块头结构必不可少:

这样,关于内存管理的核心功能就全部完成了,剩下的就是一些组合到引擎之中,构造函数,析构函数,禁用拷贝构造函数和赋值操作符之类的操作了。具体的代码我就不贴出来了,如果有疑问,可以参考这里:branch_MemoryManager

bool MemoryManager::Initialize() { static bool s_bInitialized = false; if (!s_bInitialized) { // 初始化分配器 m_pAllocators = new Allocator[k_BlockSizeCount]; for (size_t i = 0; i < k_BlockSizeCount;   i) { m_pAllocators[i].Reset(k_BlockSizes[i], k_PageSize, k_Alignment); } // 初始化查询表 m_pLookUpTable = new uint32_t[k_MaxBlockSize   1]; size_t j = 0; for (size_t i = 0; i <= k_MaxBlockSize;   i) { if (i > k_BlockSizes[j]) {   j; } m_pLookUpTable[i] = j; } s_bInitialized = true; } return true;}

游戏是个软实时系统,它要求我们必须至少每秒做30次刷新,这样才能保证基本的体验,注意,只是基本。对VR来说,要求更为严苛,至少需要每秒刷新120次才不会有眩晕感,每秒120次是什么概念?每一帧的逻辑计算 渲染的时间只有8.33ms,每一毫秒都值得我们拼劲全力去争取!

最后是释放内存,这个操作非常简单,只要把传入的指针重新加回空余块列表就行了:

 private: uint32_t m_BlockSize; // 块的尺寸 uint32_t m_PageSize; // 页的尺寸 uint32_t m_BlockCountPerPage; // 每页的块数 uint32_t m_AlignmentSize; // 对齐尺寸
void* MemoryManager::Allocate(size_t inSize){ Allocator* pAlloc = LookUpAllocator; if  return pAlloc->Allocate(); else return malloc;}void MemoryManager::Free(void* p, size_t inSize) { Allocator* pAlloc = LookUpAllocator; if  pAlloc->Free; else free;}
void* Panda::Allocator::Allocate() { if (m_pFreeList == nullptr) { // 分配一页内存 PageHeader* pNewPage = reinterpret_cast<PageHeader*>(new uint8_t[m_PageSize]); m_FreeBlockCount  = m_BlockCountPerPage; m_BlockCount  = m_BlockCountPerPage; m_PageCount  ; pNewPage->pNext = m_pPageList; m_pPageList = pNewPage; // 将所有内存块串联起来 BlockHeader* pBlockStart = m_pPageList->BlockStart(); m_pFreeBlockList = pBlockStart; for (int i = 0; i < m_nBlockCountPerPage;   i) { pBlockStart->pNext = NextBlock(pBlockStart); pBlockStart = pBlockStart->pNext; } pBlockStart->pNext = nullptr; } // 取一块内存返回 BlockHeader* pBlock = m_pFreeBlockList; m_pFreeBlockList = m_pFreeBlockList->pNext; m_FreeBlockCount--; return reinterpret_cast<void*>;}

一个分配器的数组,一张查询表。0~最大尺寸之间的所有尺寸都可以分配,什么尺寸用什么分配器则是由查询表决定的。

 BlockHeader* m_pFreeBlockList; // 空余块列表 uint32_t m_nFreeBlockCount; // 空余的块数 PageHeader* m_pPageList; // 页列表 uint32_t m_nPageCount; // 页数

分配内存块的思路是:从当前保存的空余块列表中拿一个块出来返回,如果当前没有空余的块了,那么就分配一页,将这一页上的所有块保存到空余块列表中,然后取第一块返回。为此,先添加类的成员:空余块列表、空余块数、页列表、当前已分配的页数。

#ifndef ALIGN#define ALIGN     & ~ // 获取对齐的最小值#endif// 重置分配器void Panda::Allocator::Reset(uint32_t inPageSize, uint32_t inBlockSize, uint32_t inAlignment) { FreeAll(); // 块的大小必须大于块头的大小,并且需要对齐 size_t minBlockSize = inBlockSize > sizeof(BlockHeader)? inBlockSize : sizeof(BlockHeader); m_BlockSize = ALIGN(minBlockSize, inAlignment); m_AlignmentSize = m_BlockSize - minBlockSize; // 一页至少一个块 m_PageSize = inPageSize > (m_BlockSize   sizeof (PageHeader))? inPageSize : (m_BlockSize   sizeof(PageHeader)); m_BlockCountPerPage = (m_PageSize - sizeof(PageHeader)) / m_BlockSize; }

内存管理的最重要工作是什么?内存分配的策略。管理一系列的内存分配器,收到分配内存请求的时候选择适当的分配器分配内存,然后提供一个回收内存的接口。我们采用的策略是事先初始化一堆分配不同尺寸内存分配器(因为我们不知道用户想要多大的内存),在接到分配内存的请求时,根据需要分配内存的大小,选择合适的内存分配器分配内存并返回。

图片 10图片来自网络,如有侵权,请联系作者删除

然后是具体的分配函数:

重置操作,本质上已经把这个Allocator变成了“另一个”Allocator,所以需要释放当前的所有内存,然后设置参数。设置过程中,我们要加一些限制:1、Block的尺寸必须大于等于BlockHeader的尺寸。2、每个Page必须至少包含一个块。这两条限制一加,我们就需要对输入的参数进行检查与修正。

初始化函数需要做两件事,1是创建分配器,并且将分配器全部初始化。2是初始化查询表,我们有k_BlockSizeCount个分配器,但是我们要支持1024B以下所有大小的分配,那么如果需要分配一个490B空间的内存,该用哪个分配器呢?答案是512字节的分配器。这很好理解,512B分配器是超过490B,并且最接近490B的分配器。查询表的作用就是当我输入490B的时候,我得到的是一个512B的分配器。

版权声明:本文由彩民之家高手论坛发布于编程技术,转载请注明出处:游戏引擎基础组件——内存管理