从双斜率,野人除法看性能优化

人呐,天生对线性的推崇,却在内心又对线性的不爱。一开始人们总是从最简单的线性过程入手,但最终都难免因为不满足而进入指数过程。

看一个 Linux kernel 老式 O(1) 调度器的时间片计算函数的演化:
在这里插入图片描述

早期的 O(1) 是一个连续线性函数,但不出所料最终还是变成了双斜率。

很明显,最终用双斜率分段函数替代线性函数,放大了动态范围,意思是,优先级越高,优势感越强。注意,这里体现了规模效应,基于当前的基础优先级,与当前基础优先级成比例增加时间片,而不仅仅基于优先级绝对值。

如果更平滑一点,做成多斜率分段函数,这就是指数函数了。这里道出了本质,指数函数就是多斜率分段线性函数的平滑拟合版本,这也是微积分导数的逆版本。这个本质意味着,熵减过程的最优化版本都是基于规模而不是基于绝对值的。这决定了最快的查找算法复杂度上限就是指数过程指数求解的 logn 级别。

双斜率是一个最朴素的从线性过程到指数过程的迭代过程。

为啥会这样?每个人深有体会,如果你在跑步,跑着跑着就快了,就多了,抽烟喝酒也一样,最终不可收拾。

最简单的例子就是二分法,对应 TCP 等传输协议典型的实例就是慢启动,BIC 等。此外,还有一个更加典型的例子,即不用除法的快速除法。

有点懵是吧,这一切有什么关联?不用除法如何做除法?除法只是后人总结出来的 “算法”,即一系列步骤,而不是本质。不用九九乘法表,不列竖式算除法,如何算,我们从原始人分割食物说起。参见早期我写的一篇文章 野人如何做除法

常规做法就是不同用被除数减去除数,扔掉最后的余数或者继续分余数,但显然这样效率太低,能不能加速呢?这就涉及到了规模效应,类似慢启动,“若仍然可行,则行为加倍”,来看野人除法,下面的算法来自我上面引用的文章:

#!/Users/zhaoya/myenv/bin/python3

def div1(a, divisor, depth):
    res = 1
    if depth > 6 or a == 0:
        return 0
    while divisor <= a:
        res += 1
        a -= divisor
    return res - 1 + div1(a*10, divisor, depth + 1)/10
就像跑步,跑着跑着就快了,野人祖先们不可能满足于上面低效的步骤,“若仍然可行,则行为加倍”,这是指数的指导意义,总有野人希望让那个 while 循环更快些,于是:
#!/Users/zhaoya/myenv/bin/python3

def div2(a, divisor, depth):
    res = 1
    if depth > 6:
        return 0
    elif a < divisor:
        return div2(a*10, divisor, depth + 1)/10
    b = divisor
    while 2*b <= a:
        res = 2*res
        b = 2*b
    return res + div2(a - b, divisor, depth)

如果你的知识储备足够,仔细看这两个算法的迭代过程,会发现什么,这就是 Reno TCP 到 BIC TCP 的迭代过程,div1 属于遍历逼近除数,div2 属于二分逼近除数。

核心是什么,核心不是二分法,而是 “你希望在现有基础上做增量,跑起来就想更快,要有加速度”,“现有基础” 是本质,炒股赔了的也是因为这。上面的 div2,稍微改一下成为 div3,3 个放在一起看:

#!/Users/zhaoya/myenv/bin/python3

cnt1, cnt2, cnt3 = 0, 0, 0
def div1(a, divisor, depth):
    global cnt1
    res = 1
    if depth > 5 or a == 0:
        return 0
    while divisor <= a:
        cnt1 += 1
        res += 1
        a -= divisor
    return res - 1 + div1(a*10, divisor, depth + 1)/10

def div2(a, divisor, depth):
    global cnt2
    res = 1
    if depth > 6:
        return 0
    elif a < divisor:
        return div2(a*10, divisor, depth + 1)/10
    b = divisor
    while 2*b <= a:
        cnt2 += 1
        res = 2*res
        b = 2*b
    return res + div2(a - b, divisor, depth)

def div3(a, divisor, depth):
    global cnt3
    res = 1
    if depth > 6:
        return 0
    elif a < divisor:
        return div3(a*10, divisor, depth + 1)/10
    b = divisor
    while 5*b <= a:
        cnt3 += 1
        res = 5*res
        b = 5*b
    return res + div3(a - b, divisor, depth)

r1 = div1(10000, 13, 0)
r2 = div2(10000, 13, 0)
r3 = div3(10000, 13, 0)
print(r1, r2, r3, cnt1, cnt2, cnt3)

看一下结局:

(myenv) zhaoya@zhaoyadeMacBook-Pro pytest % ./3div.py
769.23076 769.230769 769.230769 787 28 13

结果一致,但 div2 和 div3 与 div1 相比,计算步骤少了很多,这就是优化。最终,当你深入探究二分,三分,五分有什么区别时,你会发现它们都是 log 函数,这就是 O(logn) 的由来。

回过头来看,慢启动,BIC TCP,Cubic TCP,双斜率,是不是就是一回事了?

浙江温州皮鞋湿,下雨进水不会胖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值