像写诗一样撸代码是怎样的体验

目录

1. 原始版本: 2倍空间增长

2. 优化选项1: 设置一个大容器阈值,大容器采用固定增长的策略

3. 优化选项2:小容器保证扩容后一定是2^n

4. 终极完美版:信达雅的完美境界

5. 像写诗一样写代码的感觉


关于容器空间扩容问题的精巧设计,实现过采用连续空间存储的序列式容器如vector,deque的同学都知道,由于需要在插入效率和动态扩容之间取得平衡,往往需要设计一个合理空间扩容策略,来满足大多数情况下的动态增长的需求且效率损失可控。

1. 原始版本: 2倍空间增长

这是C++ STL vector采用的空间增长策略,go代码如下:

// getGrowCap get the next buffer size when grow
func (c *Container) getGrowCap() int { // V0
	return c.Cap() * 2
}

此策略代码实现简单,但是在容器size特别大的情况下,可能出现空间浪费非常严重的问题。

2. 优化选项1: 设置一个大容器阈值,大容器采用固定增长的策略

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
}

3. 优化选项2:小容器保证扩容后一定是2^n

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
}

 

4. 终极完美版:信达雅的完美境界

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
	}
}

5. 像写诗一样写代码的感觉

有人说评价一个作家水平要看他垃圾桶里的废纸,也还依稀记得王安石有过“春风又x江南岸”的纠结,最后在过/到/绿中精心雕琢,成就了“春风又绿江南岸”的佳篇。

也曾经在老仓库里无意间发现前辈们在代码里写的打油诗,很美。

如今我也在朦胧间找到了一种像写诗一样写代码的感觉,那感觉,像是在精心雕琢一件艺术品,也许这就是传说中的工匠精神,这感觉美。

码农是一件苦逼的差事,如若出于热爱回归初心,苦中作乐未尝不是一件快乐的事情。

多年之后,若有机会为学弟学妹们做一次讲演,我会说,学计算机四年毕业的时候,可以不会写程序,但一定要非常懂数学。

程序是一门艺术,不是体力活,每一段经典代码的背后,都藏着悠美的数学原理,程序之美在于数学之美。

 

你可能感兴趣的:(golang,计算机哲学,STL)