765-如何避免vector动态扩容?为什么是2倍或者1.5倍扩容?

如何避免vector动态扩容?

vector的扩容机制:当向vector插入元素时,如果元素的有效个数和空间容量相等时,vector内部会自动触发扩容机制,而扩容主要分3步骤:开辟新空间,拷贝元素,释放旧空间。
但是每次扩容时,新空间的开辟不能太大,也不能太小,太大容易造成空间的浪费,太小了则会导致扩容频繁而影响程序的运行效率。
那既然扩容会影响程序的运行效率,那我们如何来避免呢?
在插入元素之前,我们可以预估vector里面要存储多少个元素,我们提前将这个空间给它开辟好就可以了!!!
比如说,我们需要向vector中插入100个元素,在执行push_back之前,我们进行reserver预留空间,只要空间大小给的足够,在整个插入的过程中就不需要进行任何的扩容!
如果没有进行reserver预留空间,那么程序的运行结果相比执行了reserver的结果是会有很大的差别,造成边插入边扩容的情况,导致程序的运行效率极其低下!!!

我们查看到,在Windows的VS系列编译器下,是按照1.5倍的方式进行扩容,在Linux的g++中,是按照2倍的方式进行扩容的。
都是以倍数的方式来进行扩容,为什么要选择以倍数的方式进行扩容?

那接下来我们以等长个数的方式和倍数的方式,这2种方式进行对比:

对于等长个数的分布方式:也就是说,新空间的大小将为原空间大小加上一个固定的长度。
也就是说:新空间的大小:capacity+k
那如果我们向vector中插入100个元素,假如说k的值是10,我们总共需要扩容10次。而每次扩容都需要将旧空间中的元素一个一个搬移到新的空间中。
第i次扩容,需要搬移的元素的个数就是下图中的ki:
在这里插入图片描述
因为第一次我们实际上有10个大小的空间,此时新空间大小就是20个,扩容期间我们需要将旧空间中的10个元素拷贝到新的空间中。第二次扩容时,旧空间大小是20,新空间大小是30,那么我们就需要将旧空间中的20个元素一个一个拷贝到新空间中。
那么假设元素插入和元素搬移为一个单位的操作,则n个元素在它push_back期间,所需要的总的操作数就是上图中的表达式,也是下图中的表达式:
在这里插入图片描述
该表达式:n表示的是n个元素它在插入时所耗费的总的操作。
k+2k+3k+…+n/k *k,表示每次扩容我们需要搬移元素的操作,也就是搬移元素的总的操作数,然后我们对式子进行化简。然后进行均摊。
平均时间复杂度是O(N)

如果是按倍数的方式进行扩容:

假如我们有n个元素向vector中插入,倍增因子是m,在n个元素的插入期间,总共需要扩容log以m为底的n次,假如是,我们现在有1000个元素需要向vector中进行插入,而倍增因子是2,那么总共需要扩容的次数是:log以2为底的1000,来一个向上取整,也就是算出来是10,总共需要扩容10次。

在这里插入图片描述
同理,第i次扩容期间,我们的空间里已经有mi个元素,具体需要将这么多元素搬到新空间中去,因此n次push_back它所耗费的总的操作数是n+m^ 1+m^ 2+…+m^ log以m为底的n
同样的,n表示n个元素在它插入期间所耗费的总的次数,m^ 1+m^ 2+…+m^ log以m为底的n表示:等比数列,算出结果,代表n个元素以等比方式扩容所需要耗费的总操作数,然后进行均摊,单个元素所耗费的总的操作数就是表达式/n,因为m是常量,所以时间复杂度是O(1)

所以,以倍数的方式扩容比以等长的方式扩容效率要高得高

为什么是1.5或者2倍方式扩容?

而不是3倍,4倍的方式扩容?
扩容的机制:开辟新空间,拷贝元素,释放旧空间。
而理想的方案是我在下一次扩容的时候,如果刚好可以利用前n-1次所释放的空间,那就太好了。
假如说我们是以2倍方式扩容(1,2,4,8,16),则第i次扩容期间所需要的空间总量就是2^i次方,如果第4次扩容时总共需要8个元素大小的空间,但是前3次已经释放的空间加起来的总量,刚好是7,而7小于8,不足以我们第4次扩容时所需要的空间,也就是说,如果恰巧以2倍方式扩容,那么每次扩容时前面释放的空间它都不足以支持本次的扩容!!!那么如果是以更高倍数的方式进行扩容,则这个空间它的浪费情况就会更高!!!
也就是说,以2倍或者更高倍数的方式进行扩容,会存在2个问题:
1、空间浪费可能比较大
2、无法使用前面已经释放的空间

STL标准没有严格说明我们应该按照哪一种方式进行扩容,因此不同STL的实现产商都是按照自己的方式进行扩容的。
各个版本的STL在实现时,必须要结合所在操作系统的堆空间管理方式,才能更高效的来进行使用。
我们查看到,在Windows的VS系列编译器下,是按照1.5倍的方式进行扩容,在Linux的g++中,是按照2倍的方式进行扩容的。

为什么VS编译器以1.5倍方式扩容?

各个版本的STL在实现时,必须要结合所在操作系统的堆空间管理方式,才能更高效的来进行使用。
在Windows中,对于已经释放的或者是没有被申请的堆空间空闲内存块,主要采用堆表的方式进行管理,而这个堆表主要有2种:空闲双向链表和快速的单向链表。
对于空闲双向链表:主要是按照内存块大小的不同被分成128条,每条列表中对应的都是循环双向链表的结构。free[1]~free[127]主要管理的是以8的整数倍的空闲内存块,free[0]主要管理的是大于等于1024个字节而小于等于512字节的堆块,
在这里插入图片描述

对于快表: 是Windows用来加速堆块分配采用的一种堆表,之所以称为快表,是因为这个快表里的内存块是永远不会进行合并的,而且每条链表中最多只能挂4个节点
在这里插入图片描述
以上就是Windows下对于这些空闲块的管理方式
那么经过反复的申请和释放出来以后,堆空间已经“千疮百孔”,可能还会产生一些碎片,那为了合理的有效利用一些空闲的内存块,或者说为了提高用户申请扩容的成功率,那这个堆管理器它需要对空间块进行适时的合并,当堆管理器发现2个空闲块彼此相邻的时候,就会对堆块进行合并,假如说,我们有512个字节的空闲块,连续7次申请64个字节之后,还有一个64个字节的空闲内存块没有被使用,那么在程序的运行的过程中,假如说有2个内存块被提前释放了,也就是说,现在有3个空闲的内存块,那在这一刻这个程序又需要128个字节的内存块了,虽然说我们程序里面现在有3个64字节的空闲块,也就是说有194个字节的空闲内存块,但是由于它们在里边可能不连续,还不能来直接进行分配,那这个操作系统在发现了空间不够之后,它会对这些空闲内存块进行扫描,如果发现2个空闲内存块之间是连在一起的,那就会将这些空闲内存块进行合并,那我们这个位置上给到64个字节,它3个64字节刚好是连在一块的,堆管理器就会堆对它来进行合并,合并成1个更大的内存块来进行分配,这个就是Windows底下对空间的大概的管理方式。在这里插入图片描述
这里由于Windows下它会实时的通过堆管理器对这些空闲内存块进行合并,因此我们认为这是它在vector里边采用1.5倍方式进行扩容的原因。
因为只要进行了这种合并,多次扩容之后就可以重复复用之前已经释放的空间了。

那为什么Linux又是选择以2倍方式扩容?

Linux主要采用glibc中的ptmalloc来进行空间的申请,而这个malloc它申请的空间如果小于128kb,就会通过内部的brk来进行空间的扩增。如果大于128kb,但是arena里边又没有足够的空间时,是通过mmap的这种系统调用来进行内存空间的映射的,arena是什么?我们可以理解为是一块内存区域
在这里插入图片描述

在这里插入图片描述
那么,我们可以在这个内存区域里面进行空间的申请,对于申请出来的每一个小块内存都对应一个chunk,chunk是一个内存块的意思,根据这个内存块的作用的不同,chunk总共分为4类:
在这里插入图片描述
smallbins:主要用来管理用户已经归还的一些小块内存的,smallbins如果没有找到,就会从last remainder chunk里面进行切割。

下图是chunk结构
在这里插入图片描述
左图是被申请的allocated chunk,右图是用户已经归还的chunk,也就是说可以被再一次申请了。
从这个内存块中我们可以看到,假如说用户需要40个字节,实际上系统给我们分配的内存块远远大于40个字节,因为当一个内存块用完free之后,这个内存块不会直接还给操作系统,而是把它当作一个free chunk管理在这个bin里面。bin主要采用的是空闲链表的方式对这些空闲块来进行管理,bin它也被分为了许多个。根据作用的不同,首先有10个fast bin,

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
那既然Linux里边对这些空闲块进行管理期间也会进行合并,为什么选择2倍方式扩容而不是选择1.5倍方式扩容?在我们之前了解到,以2倍方式扩容是不能复用之前已经释放的空间,如下图:
在这里插入图片描述
在这里插入图片描述
log以2为底的request代表用户所要请求或者说释放内存块的大小
basicSize代表的是一个基本单位的大小
假如说,我们这个位置(左图蓝色空间)有1M的内存空间,而基本的大小在里面是1k,右侧给了11个元素的数组,这个数组将来的每一个位置存放的都是一个链表,0号位置主要管理的是1k字节的内存大小,2号位置管理的是2的2次方,4k字节的内存大小,9的位置主要管理的是2的9次方,也就是512字节的内存大小,在初始状态,我们这个内存空间(左图)没有进行分配,因此0-9这10个区域之间没有挂接任何的内存块,而在10号位置挂接了一个内存单元,这个内存单元的起始地址是0,也就是我们所给的这个1M空间(左图)的起始地址。
那接下来假如说A程序要申请96kb的内存块,按照上图中给出的哈希函数,log以2为底的96刚好介于64和128之间,对它进行向上的取整,来到了128,而128是2的7次方,到2的7次方这个位置一看,没有内存块可供使用,那就向上找,也没有,再向上查找,也没有内存块可进行使用,再向上查找,在这个10号的空闲链表中,它挂了一个,它有一个内存块,这个内存块是0号位置的这个整个的空间,那么接下来我们就对这整块空间把它先一分为二,分成2个512,底下的我将要分配给A程序来进行使用,上面的这个512就把它管理在9号的空闲链表中,
在这里插入图片描述
但是底下的512直接分配给用户浪费有点多,所以我们再对512进行均分,划分成2个256,底下的这个256将来交给用户来使用,上面的这个256把它继续挂接在8号空闲链表中。因为2的8次方刚好是256。这个256也代表的是左图划分的内存空间在1M空间的起始地址。那接下来底下的这个256要把它分配给用户来使用,但是用户要的是96k,你直接给一个256太浪费了,因此再来一个均分,分成2个128,底下的这个128分给用户来使用,上面的这个128继续挂接在7号空闲链表中,这个就是A程序申请96字节的一些操作步骤。

假如说,我们这个B程序它又需要申请60个字节,这60个字节按照哈希函数,log以2为底的60次方向上取整刚好是64,也就是6号位置,这个链表没有对应的内存块,向上走,找一个更大的内存块来进行切割,就会把7号位置的128切割,一分为二,划分成2个64kb,
在这里插入图片描述
在这里插入图片描述
底下的这个64kb假设把它交给B程序来使用,上面的这个64kb把它放到对应的6号空闲链表中,把它管理起来就OK了。这个就是B程序的申请过程。

假如说C程序同样它又要申请78个字节,78介于64和128之间,向上取整,取到128,对应的是7号位置,但是7号位置没有内存块,同样的向上进行查找,发现了一个256字节的内存块,就对这个256进行均分,一分为二,
在这里插入图片描述
底下的128交给C程序使用,上面的这个128放在7号单元中管理就可以了。
因为8号的256字节的内存块已经倍分割了,所以8号这个位置它就没有再挂接任何的内存块了。

过了一段时间后,假如说我们这个A程序执行结束,既然执行结束,就需要将A程序的所拥有的128个字节给它释放掉,它一释放,同样的,找我们刚才的哈希函数,log以2为底的128,刚好是2的7次方,就把这个内存块插在7号链表中,在这里插入图片描述
因为这个128和上面的128不是连续的,因此这2个空间还不能进行合并。

接下来这个D程序需要申请40个字节的内存块,log以2为底的40次方向上取整,来到了64,6号位置刚好有在里边有1个内存块,既然有一个内存块,就把这个64kb交给程序D进行使用就可以了。还需要把6号里边中的64kb移除掉。

在这里插入图片描述
比如说,B程序执行结束了,因为B刚才申请60个字节,也就是说, 它要把申请的64字节归还掉。只能归还在6号位置,在6号位置的空闲链表中把它挂接起来就可以了。
在这里插入图片描述
如果D程序在执行结束了,发现给D分配的也是64kb,要进行归还,把它插在6号位置的空闲链表中,但是这个6号空闲链表中有了一个内存块,而且这2个内存块是连在一起的,那么就会将D的64和刚才所归还的64进行合并,任何把它管理起来就可以了。
在这里插入图片描述
而这个128刚好和7号空闲链表的里边的0的这个内存块,它们两个又是连续的,把这2个128合并成1个256,挂接在这个8号位置就可以了,因为上面的这个128不连续,所以说没有办法进行合并,仍旧在7号位置中。

C程序如果此时结束了,它需要对它所管理的一个256来进行释放,归还到8号位置,C原本申请的是78,78申请的是128,把128挂接在7号位置,7号位置的链表的384和C程序所归还的128刚好也是连续的。就把这2个内存块合并,合并后挂接到8号位置,而8号位置的内存单元中128和C程序归还的128是连续的,合并成一个512放在9号位置中,而左图的512和这个512是连续的,再合并,最终挂接在10号位置的空闲链表中,在这里插入图片描述
这个就是伙伴系统大概的空间申请和释放的方式,通过哈希的处理方式来进行空间的定位,非常高效,而且对这个空间进行切分以及合并的时候,效率也非常高,而且它每一次在空间分配的时候,都是以basic size的2的i次方进行空间的分配,这就是SGI-STL选择2倍方式扩容的原因。在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值