动态储存管理小教程

2012-1-9 

其实这个不是真正正统的教程(甚至可以认为是不务正业),因为我使用的语言是JASS语言 即魔兽争霸3地图编辑器的脚本语言。(好吧 我知道石头你又该嘲笑我了。确实 整这么长时间我一张成型的图也没拿出来。。。。但是,你不能否认我的技术!!

但是其中算法和思想。其实和操作系统中储存管理是没啥区别的。在家各种冷,没事码码字活动活动僵硬的手指。
正好也尝试以浅显易懂的方式写一下自己小小的研究成果。

JASS这个语言是很简陋的(PASCAL风格)。他本身的数组全都固定是8192长度的。这样利用率非常的低。所以我就想做一套动态分配的数组。可以自定义数组长度提高利用率。所以我这个系统应运而生。

原理概要

jass中提供的数组都是8192长度。如此的规整因此我们可以把他看成一条内存。以下称作内存数组
实际上动态数组就是一个内存分配与回收的过程。就好比切一块长条的吐司面包。把最初是一块整的面包,逐渐切成根据具体需要的小块。而不是一口吞下。因此一个动态数组就是一个内存块。如何申请动态数组就是如何切面包的问题。(下文会交替使用内存块和面包块大家知道指的是一个东西就行)

首先。我要管理这个面包,我必须要了解他。这里我要给每块内存块记录一些信息。这样我才能更好的分配他们给大家吃。
控制信息如下。
     struct block
    {   int state;//1 :内存块状态----1=占用、 0=空闲  
        int initaddr;//2:内存块头地址 也就是这块空间在内存数组中的起始索引
        int size;//3:内存块大小
        int prev;//4:前相邻内存块索引(前指针)
        int next;//5:后相邻内存块索引(后指针)
    }



这里面我是用C语言的struct表示。在jass中其实就是以5个连续整数为整体放到数组中的,这个数组姑且叫做目录表。其实就是一个清单,纪录目前都有哪些面包块,都是多大。是分配给烧饼了还是分配给山炮了。或者是还没有分配。
然后这些控制信息节点在目录表中组成了一个双向链表。即知道自己的邻居在哪。能够方便的找到相邻的内存块。
1、申请

发面包了!首先我要从一大整块面包切一刀(这一大整块面包是目录表初始生成的一个节点)。然后把切出来的一块同时记录各种信息放到目录表里。然后分配给用户一个id 就像这样 integer a=allocArray(INT,200) 
这样a就是一个200大小的整型数组了。这个a其实就是对应的内存块的控制信息节点的序号或者说索引。(因为我一个节点是包含5个数据的因此实际的索引是id*5再加上目录表初始位置)。这样就能通过目录表中的内存块信息。存取这段(数组)内存了。
存取就象这样 LoadInt(id,index);SaveInt(id,index,value)
2、回收

由于来面包party人比较多。你吃了面包,一定要自己再烤一块放到面包盘里供其他食用。否则后来到的人岂不是要饿死了。
做面包虽然复杂但对于咱们的内存块就简单多了。直接把内存块状态从占用改为空闲不就行了么!
真的这么简单?
确实是这么简单,但同时带来很大的问题。随着吃面包的人越来越多。原来那一整块面包被切的越来越碎。这样一来,后面如果来了饭量大的。每块面包都不够他吃但我们的规则又是只能拿一块。那么这个倒霉蛋是不是要注定饿死了。因此,当一个人吃完了之后又烤好新的放回去的时候。他要看下左右有没有别人新烤好放回去的面包,如果有,那他就应该回炉合并这两块或者三块面包组成一个更大块的面包。这样前面的杯具就不会发生了。这样就是释放做的最大的工作,合并相邻的空闲内存块。这里我的系统使用了一个策略即向右合并。即总是把左边的内存块合并到又边去,同时删掉左边的节点。如ABC三块A和C是空闲块。B是即将释放的。这是后会把A删掉各种信息合并到B里。变成B'C然后删掉B'把B'再合并到C里。
这样的好处是什么呢?因为我切面包总是从左开始切,那么那块面包剩余的部分(一般是较大的部分)总是在右边。这样的策略,总是能优先把内存释放到最大的那部分里。
不得不提的是,使用数组结构储存的另一大有点。上面所说的最大部分的面包在物理上总是存在数组的最左边的。而搜索可用内存块时又是从左边搜索。这样一来,分配总是从那块大的分配,而释放又总是释放到那块大的里面。这样就大大提高了内存的吞吐率,减少了内存碎片的产生。
3、存取

这个系统的重点就在于上述的分配与回首。当然存取也不得不提。
我这个系统内存理论上是没有长度限制的。我把多个数组拼成一个线性地址空间了。只需索引对8191取模即可 当然还需要分支语句使用这几个数组
具体看实际代码吧
4、相关技术

    下面我讲一下目录表表本身的存储机制
    其实我是把他们存到一个特殊的栈里的。这种栈只能在栈顶添加节点。但是允许在任意位置删除节点。
    原理是,栈顶指针指的是逻辑上的栈顶并不代表是物理上的顶端。举例如下【sp:stack pointer 栈顶指针】
     0   1  2  3   4   5  6                                                               0  1   2   3   4  5   6  7   8
    [1][2][3][4][0][0][0]  此时sp=4 如果删除第2个元素则变成 [1][(4)][3][4][0][0][0][0][0] 
                   I                                                                                   I
    这时sp=1即指向刚刚被删除的那个元素。但是刚才那个元素要储存上个栈顶的位置。用来指导sp的移动。
    也即是说这种情况下,如果新数据到来,会存到第2个元素这里。同时sp为指向第2元素所指导的位置。即索引4.
    以此类推。
    当然需要说明的是。我的元素不止一个字段。必须有字段来表示这个位置是否是空的。
    不然无法判断该位置是所存的4是业务数据还是栈顶指导数据。因此使用了块大小那个字段。
    如果扫描节点时遇到一个节点的块大小是-1,则此节点是被删掉的节点,即【SP位移指导节点】(好吧自己胡乱起的名字)。
    注:如果在需要位移指导时没有发现【SP位移指导节点】(其实就是初始的情况 如图1sp指的那个位置是个空节点没有指导信息)
    。那么此时sp=sp+1。和普通栈一样。


这种算法的好处就是利用本身的字段空间储存一些信息。而不用进行栈顶元素以为补空。但是对于单元素节点的表不适合此方法。
5、struct vector 与类型检查

我的代码中并未提供struct解决方案(这里是想在jass语言里实现struct封装)
但是我基于这个系统可以提供一个思路。
如struct
{
  int a;
  int b;
  real c;
unit u
}
则可以这样 
integer a=allocArray(INT,2)//假设此时a=3
real b=allocArray(REAL,1)//b=11
unit u=allocArray(UNIT,1)//u=321
然后将a、b、u这3个数封装为一个数。即3210011003做为这个struct的id
然后使用这个id存取对用类型数据时再对这个数解析取出符合该类型的数就行了。
为什么要同时说下类型检查呢 因为思路和这个是一样
我的动态数组分配的id号本身是无法表示类型的如果你申请了个整型的数组id用来存取unit的起步乱套了。
因此同理 我把类型代号封装到这个id里 存取时先检查类型。
当然这些所谓的类型检查是运行期检查而已 ,很没有意义。

也正如struct的意义在于一个新的语言支持(像vj)一样。
这些东西,如果我做一个新语言,实现也就显得有那么些价值了。编译器类型检查也比这方便效率多了。

globals  
     //内存地址空间 
     integer   array  IntArray1 
     integer   array  IntArray2 
     real   array  RealArray1 
     real   array  RealArray2 
     string   array  StrArray1 
     string   array  StrArray2 
     unit   array  UnitArray1 
     unit   array  UnitArray2 
     integer   array  ContentTable //目录表 记录着系统中内存块的信息。 
     //每块内存信息作为一个节点链式储存在这个数组中 
      integer  CTRLINFO_LEN= 5 //内存块控制字段的长度 具体含义上面已经有解释了 

     integer   MAXTYPE= 4 //这个是支持的类型的最大数目。系统会根据这个为每个类型划分目录表的存储区间 
     //目前只是用了几个常用类型如下 如果需要添加则增加MAXTYPE的值并添入类型代号即可 注意类型代号必须是依次的。 
     //例如我想加一个新类型timer 则定义integer TIMER=4 
     integer  INT= 0 //代表整形 
     integer  REAL= 1  
     integer  STRING= 2  
     integer  UNIT= 3  

     integer  test= 1  
endglobals  


function  initArray  takes   nothing   returns   nothing  
//初始化内存块。也就是分配第一个内存块。 
     local   integer  atype= 0  
     local   integer  initaddr 
     loop  
         exitwhen  atype>=MAXTYPE 
         set  initaddr=atype*R2I ( 8000 /MAXTYPE ) +atype 
         set  ContentTable [ initaddr ] = 1  
         set  ContentTable [ initaddr+ 2 ] = 0  
         set  ContentTable [ initaddr+ 3 ] = 16000   //寻址两个数组长度 
         set  ContentTable [ initaddr+ 4 ] = -1  
         set  ContentTable [ initaddr+ 5 ] = -1  
         set  atype=atype+ 1  
     endloop  
endfunctionfunction dump  takes   string  str  returns   nothing  
     call  DisplayTextToPlayer ( GetLocalPlayer ( ) , 0 , 0 ,str )  
endfunction  



function  searchBlock  takes   integer  asklen, integer  initaddr  returns   integer  
     //寻找第一个满足分配要求的内存块 大多数情况总会是第一块最大的。(因为他就在位置0) 
     local   integer  n= 0  
         loop  
             exitwhen  ContentTable [ initaddr+n*CTRLINFO_LEN+ 3 ] == 0  
             //因为不存在内存块大小为0的内存块(被删除的块大小为-1)因此遇到这个情况说明搜索到目录表物理末端了 
             if ( ContentTable [ initaddr+n*CTRLINFO_LEN+ 1 ] == 0   and  ContentTable [ initaddr+n*CTRLINFO_LEN+ 3 ] >=asklen )   then  
                 return  n 
             endif  
             set  n=n+ 1  
         endloop  
         return   -1  

endfunction  


function  setPrevBlock  takes   integer  initaddr, integer  id, integer  previd  returns   nothing  
     //设置内存块id的前接内存块为previd 
     set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 4 ] =previd 
endfunction  
function  getPrevBlock  takes   integer  initaddr, integer  id  returns   integer  
     //获取内存块id的前内存接块 
     return  ContentTable [ initaddr+id*CTRLINFO_LEN+ 4 ]  
endfunction  


function  setNextBlock  takes   integer  initaddr, integer  id, integer  nextid  returns   nothing  
     //设置 内存块id的后接内存块为nextid 
     set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 5 ] =nextid 
endfunction  
function  getNextBlock  takes   integer  initaddr, integer  id  returns   integer  
     //获取内存块id的后接内存块 
     return  ContentTable [ initaddr+id*CTRLINFO_LEN+ 5 ]  
endfunction  


function  removeBlock  takes   integer  id, integer  atype  returns   nothing  
     //删除一个已经合并到其他空闲块的内存块。 
     local   integer  initaddr=atype*R2I ( 8000 /MAXTYPE ) +atype 
     local   integer  previd 
     local   integer  nextid 
     if ( id== 0 )   then  
            call  dump ( "严重警告 第0块被删除了" ) //因为第0块其实是最右边的那块 因此不可能被删除 
      
     endif  
      if ( ContentTable [ initaddr+id*CTRLINFO_LEN+ 3 ] == 0 )   then  
         call  BJDebugMsg ( "无法删除一个不存在的节点" )  
         return  
     else  
         set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 1 ] =ContentTable [ initaddr ]  
         set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 3 ] = -1 //块大小变成-1 表示此节点变成sp位移指导节点 
         set  ContentTable [ initaddr ] =id 
         //以下是标准的双向链表删除节点操作 学过数据结构的都应该清楚 
         //就是把前节点的后指针指向该节点的后节点 
         //把后节点的前指针指向该节点的前节点 
         set  previd=getPrevBlock ( initaddr,id )  
         set  nextid=getNextBlock ( initaddr,id )  
         if ( previd>= 0 )   then  
             call  setNextBlock ( initaddr,previd,nextid )  
         endif  
         if ( nextid>= 0 )   then  
             call  setPrevBlock ( initaddr,nextid,previd )  
         endif  
      endif  

endfunction  

  function  allocArray  takes   integer  len, integer  atype  returns   integer  
     local   integer  initaddr=atype*R2I ( 8000 /MAXTYPE ) +atype 
     local   integer  nid= -1  
     local   integer  bid=searchBlock ( len,initaddr ) //找到待切的蛋糕 

     if ( bid== -1 )   then  
         call  MemDefrag ( atype )   
         //内存不足 
         call  dump ( "内存不足!" )  
     else  
         set  nid=ContentTable [ initaddr ]  
         if ( nid> 8000 /MAXTYPE/ 5 )   then  
             call  dump ( "内存块数量溢出!" )  
             return   -1  
         endif  
         if ( ContentTable [ initaddr+nid*CTRLINFO_LEN+ 1 ] == 0 )   then //处理无sp指导信息时的sp步进位移 
             set  ContentTable [ initaddr ] =ContentTable [ initaddr ] +1  
         else  
             set  ContentTable [ initaddr ] =ContentTable [ initaddr+nid*CTRLINFO_LEN+ 1 ] //否则sp位移到指导信息所标识位置 
         endif  

         set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 1 ] = 1  
         set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 2 ] =ContentTable [ initaddr+bid*CTRLINFO_LEN+ 2 ]  
         set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 3 ] =len 
         set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 4 ] =ContentTable [ initaddr+bid*CTRLINFO_LEN+ 4 ]  
         if ( ContentTable [ initaddr+bid*CTRLINFO_LEN+ 4 ] != -1 )   then  
             set  ContentTable [ initaddr+ContentTable [ initaddr+bid*CTRLINFO_LEN+ 4 ] *CTRLINFO_LEN+ 5 ] =nid 
         endif  
         set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 5 ] =bid 
         //set test=test+1 
         if ( ContentTable [ initaddr+bid*CTRLINFO_LEN+ 3 ] -len== 0 )   then //如果正好分配完 老内存块删掉 
                 call  removeBlock ( bid,atype )  
         else  
             set  ContentTable [ initaddr+bid*CTRLINFO_LEN+ 2 ] =ContentTable [ initaddr+bid*CTRLINFO_LEN+ 2 ] +len 
             set  ContentTable [ initaddr+bid*CTRLINFO_LEN+ 3 ] =ContentTable [ initaddr+bid*CTRLINFO_LEN+ 3 ] -len 
             set  ContentTable [ initaddr+bid*CTRLINFO_LEN+ 4 ] =nid       
         endif  

     endif  

     return  nid 
endfunction  
function  destroyArray  takes   integer  id, integer  atype  returns   nothing  
     local   integer  initaddr=atype*R2I ( 8000 /MAXTYPE ) +atype 
     local   integer  flag= 4  
     local   integer  nid 

     set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 1 ] = 0  
     set  nid=ContentTable [ initaddr+id*CTRLINFO_LEN+ 4 ]   //合并前接空闲块 
      if ( nid>= 0   and  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 1 ] == 0 )   then  
         set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 2 ] =ContentTable [ initaddr+nid*CTRLINFO_LEN+ 2 ]  
         set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 3 ] =ContentTable [ initaddr+id*CTRLINFO_LEN+ 3 ] +ContentTable [ initaddr+nid*CTRLINFO_LEN+ 3 ]  
         set  ContentTable [ initaddr+id*CTRLINFO_LEN+ 4 ] =ContentTable [ initaddr+nid*CTRLINFO_LEN+ 4 ]  
         call  removeBlock ( nid,atype )  
     endif  
     set  nid=ContentTable [ initaddr+id*CTRLINFO_LEN+ 5 ] //合并后接空闲快 
     if ( nid>= 0   and  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 1 ] == 0 )   then  
          set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 2 ] =ContentTable [ initaddr+id*CTRLINFO_LEN+ 2 ]  
          set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 3 ] =ContentTable [ initaddr+nid*CTRLINFO_LEN+ 3 ] +ContentTable [ initaddr+id*CTRLINFO_LEN+ 3 ]  
          set  ContentTable [ initaddr+nid*CTRLINFO_LEN+ 4 ] =ContentTable [ initaddr+id*CTRLINFO_LEN+ 4 ]  
         call  removeBlock ( id,atype )  
     endif  

endfunction  
function  CountRealIndex  takes   integer  atype, integer  id, integer  index  returns   integer  
//计算真实的索引 
     local   integer  initaddr=atype*R2I ( 8000 /MAXTYPE ) +atype 
     local   integer  loc=ContentTable [ initaddr+id*CTRLINFO_LEN+ 2 ] +index 
     if ( index>=ContentTable [ initaddr+id*CTRLINFO_LEN+ 3 ]   or  ContentTable [ initaddr+id*CTRLINFO_LEN+ 1 ] == 0 )   then  
         call  dump ( "非法的索引:" +I2S ( index ) )  
     endif  
     return  loc 

endfunction  
function  LDI  takes   integer  id, integer  index  returns   integer   //loadinteger 
     local   integer  loc=CountRealIndex ( INT,id,index )  
    if ( loc>= 8100 )   then  
         return  IntArray2 [ loc- 8100 ]  
     else  
         return  IntArray1 [ loc ]  
     endif  

endfunction  
function  SVI  takes   integer  id, integer  index, integer  val  returns   nothing   //saveinteger 
     local   integer  loc=CountRealIndex ( INT,id,index )  
    if ( loc>= 8100 )   then  
        set  IntArray2 [ loc- 8100 ] =val 
     else  
        set  IntArray1 [ loc ] =val 
     endif  
endfunction  

你可能感兴趣的:(管理)