目录
1. 原始版本: 2倍空间增长
2. 优化选项1: 设置一个大容器阈值,大容器采用固定增长的策略
3. 优化选项2:小容器保证扩容后一定是2^n
4. 终极完美版:信达雅的完美境界
5. 像写诗一样写代码的感觉
关于容器空间扩容问题的精巧设计,实现过采用连续空间存储的序列式容器如vector,deque的同学都知道,由于需要在插入效率和动态扩容之间取得平衡,往往需要设计一个合理空间扩容策略,来满足大多数情况下的动态增长的需求且效率损失可控。
这是C++ STL vector采用的空间增长策略,go代码如下:
// getGrowCap get the next buffer size when grow
func (c *Container) getGrowCap() int { // V0
return c.Cap() * 2
}
此策略代码实现简单,但是在容器size特别大的情况下,可能出现空间浪费非常严重的问题。
V1版本出于对容器现有size边界值的考虑,当oldCap=capTooLarge-1的情况下,容器实际只增长1,明显有漏洞
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V1
oldCap := c.Cap()
newCap := oldCap * 2
if oldCap >= capTooLarge { // too large, grow by capTooLarge
newCap = (oldCap/capTooLarge + 1) * capTooLarge
}
return newCap
}
V2版本增加最少增长capTooLarge*50%的条件判断,有效修复了V1的漏洞
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V2
oldCap := c.Cap()
newCap := oldCap * 2
if oldCap >= capTooLarge { // too large, grow by capTooLarge
newCap = (oldCap/capTooLarge + 1) * capTooLarge
// at least grow 50% of capTooLarge
if (newCap - oldCap) < (capTooLarge / 2) {
newCap += capTooLarge
}
}
return newCap
}
V3版本实现了小容器扩容后保证是2^n的需求
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V3
oldCap := c.Cap()
newCap := oldCap * 2
for i := 1; ; i *= 2 { // newCap=>2^n
if i >= newCap {
newCap = i
break
}
}
if oldCap >= capTooLarge { // too large, grow by capTooLarge
newCap = (oldCap/capTooLarge + 1) * capTooLarge
// at least grow 50% of capTooLarge
if (newCap - oldCap) < (capTooLarge / 2) {
newCap += capTooLarge
}
}
return newCap
}
V4版本在v3的基础上,优化了2^n判断逻辑的效率。
基于的数学原理是 x&(x-1)能去除二进制位中最低位的1,如 10110&10101=10100。
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V4
oldCap := c.Cap()
newCap := oldCap * 2
if newCap&(newCap-1) != 0 { // newCap!=2^n, then newCap=>2^n, eg: 3->6->8
for newCap&(newCap-1) != 0 {
newCap = newCap & (newCap - 1) // remove the last binary digit 1, eg: 10110->10100
}
newCap *= 2 // round up
}
if oldCap >= capTooLarge { // too large, grow by capTooLarge
newCap = (oldCap/capTooLarge + 1) * capTooLarge
// at least grow 50% of capTooLarge
if (newCap - oldCap) < (capTooLarge / 2) {
newCap += capTooLarge
}
}
return newCap
}
V5去掉了大容器最小增长的条件判断,将这个逻辑通过一个合并的公式实现
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V5
oldCap := c.Cap()
newCap := oldCap * 2
if newCap&(newCap-1) != 0 { // newCap!=2^n, then newCap=>2^n, eg: 3->6->8
for newCap&(newCap-1) != 0 {
newCap = newCap & (newCap - 1) // remove the last binary digit 1, eg: 10110->10100
}
newCap *= 2 // round up
}
const ctl = capTooLarge
if oldCap >= ctl { // too large, grow by capTooLarge
newCap = ((oldCap+ctl/2)/ctl + 1) * ctl // at least grow 50%*capTooLarge
}
return newCap
}
V6在v5的基础上优化了小容器重复计算newCap&(newCap-1)的问题,优化了计算速度。
大容器公式采用缩写是公式更有可读性。
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V6
oldCap := c.Cap()
newCap := oldCap * 2
if t := newCap & (newCap - 1); t != 0 { // newCap!=2^n, then newCap=>2^n, eg: 3->6->8
newCap = t * 2 // round up
// loop to remove the last binary digit 1, eg: 10110->10100->10000
for t = newCap & (newCap - 1); t != 0; t = t & (t - 1) {
newCap = t
}
}
const ctl = capTooLarge
if oldCap >= ctl { // too large, grow by capTooLarge
newCap = ((oldCap+ctl/2)/ctl + 1) * ctl // at least grow 50%*capTooLarge
}
return newCap
}
V7优化小容器代码为完美的for循环,进一步增强代码可读性
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V7
oldCap := c.Cap()
newCap := oldCap * 2
// if newCap!=2^n, then newCap=>2^(n+1), eg: 3=>6=>8
// loop to remove the lowest binary digit 1, eg: 10110=>10100=>10000
t := 2 * newCap
for t &= (t - 1); t != 0; t &= (t - 1) {
newCap = t
}
const m = capTooLarge // 4096
if oldCap > m { // too large, grow by capTooLarge
// at least grow 50% of capTooLarge
newCap = ((oldCap+m/2)/m + 1) * m
}
return newCap
}
V8优化代码冗余执行路径,进一步增强代码可读性,整个实现达到了信达雅的完美境界。
// nextCap get the next buffer size when grow
func (c *Container) nextCap() int { // V8.final
if oldCap := c.Cap(); oldCap < capTooLarge { // little mode, 2*oldCap=>2^n
newCap := oldCap * 2
// if newCap!=2^n, then newCap=>2^(n+1), eg: 3=>6=>8
// loop to remove the lowest binary digit 1, eg: 10110=>10100=>10000
for t := 2 * (newCap & (newCap - 1)); t != 0; t &= (t - 1) {
newCap = t
}
return newCap
} else { // large mode, grow by capTooLarge, at least +50%*capTooLarge
const m = capTooLarge // 4096
return ((oldCap+m/2)/m + 1) * m
}
}
有人说评价一个作家水平要看他垃圾桶里的废纸,也还依稀记得王安石有过“春风又x江南岸”的纠结,最后在过/到/绿中精心雕琢,成就了“春风又绿江南岸”的佳篇。
也曾经在老仓库里无意间发现前辈们在代码里写的打油诗,很美。
如今我也在朦胧间找到了一种像写诗一样写代码的感觉,那感觉,像是在精心雕琢一件艺术品,也许这就是传说中的工匠精神,这感觉美。
码农是一件苦逼的差事,如若出于热爱回归初心,苦中作乐未尝不是一件快乐的事情。
多年之后,若有机会为学弟学妹们做一次讲演,我会说,学计算机四年毕业的时候,可以不会写程序,但一定要非常懂数学。
程序是一门艺术,不是体力活,每一段经典代码的背后,都藏着悠美的数学原理,程序之美在于数学之美。