You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Python环境下分组合并累加分数比线性累加更快的性能差异原因咨询

Python环境下分组合并累加分数比线性累加更快的性能差异原因咨询

嘿,我最近在折腾Python里高精度分数累加的性能优化,遇到了一个让我困惑的现象——用**分组合并(归并式两两配对累加)**的方式计算分数和,居然比直接线性逐个累加要快将近40%!虽然理论上两种方式的总加法运算次数是一样的(都是n-1次分数加法,n是分数个数),但实际跑出来的性能差了不少。先给大家看看我的测试代码和结果,再聊聊我琢磨出的原因~

测试代码

from itertools import batched
from math import gcd

def arctan_derivative(x: int, y: int) -> tuple[int, int]:
    return y * y, x * x + y * y

def primitive_rationals_simple(denominator: int) -> list[tuple[int, int]]:
    assert isinstance(denominator, int) and denominator > 0
    result = []
    for numerator in range(denominator + 1):
        common = gcd(numerator, denominator)
        result.append((numerator // common, denominator // common))
    return result

def make_test_case(n: int) -> list[tuple[int, int]]:
    return [arctan_derivative(*frac) for frac in primitive_rationals_simple(n)]

# 线性累加方式
def test1(fracs: list[tuple[int, int]]) -> tuple[int, int]:
    num, den = 0, 1
    for tnum, tden in fracs:
        num, den = num * tden + tnum * den, den * tden
    return num, den

# 分组合并累加方式
def test2(fracs: list[tuple[int, int]]) -> tuple[int, int]:
    while (length := len(fracs)) > 1:
        carry = fracs.pop(-1) if length & 1 else None
        fracs = [(a * d + b * c, b * d) for (a, b), (c, d) in batched(fracs, 2)]
        if carry:
            fracs.append(carry)
    return fracs[0]

测试结果

data = make_test_case(360)
print(test1(data) == test2(data))  # 输出: True(结果一致)

%timeit test1(data)  # 271 μs ± 9.95 μs per loop
%timeit test2(data)  # 166 μs ± 948 ns per loop
print((271 - 166)/271)  # 输出: ~0.387,快了将近40%

为什么分组合并更快?

我一开始完全摸不着头脑,后来仔细琢磨Python解释器的工作机制,总结出这几个关键原因:

  • 列表推导式 vs Python级for循环:底层实现的效率鸿沟
    test1用的是纯Python层面的for循环,每次迭代都要在Python解释器里执行变量赋值、算术运算等操作,循环n次就有n次Python级别的迭代开销。而test2用了列表推导式,它的内部循环是用C实现的,批量处理所有分数对的加法,能大幅减少Python解释器的循环开销。比如n=360时,test1要跑360次Python循环,而test2只需要跑log₂(360)≈9次列表推导式,每次推导式的内部循环是C级别的,效率差不止一个量级。

  • 内置函数batched的高效性
    test2里的itertools.batched是Python 3.12+的内置函数,同样是C实现的,能高效地把列表分成两两一组,没有额外的Python级别的配对逻辑开销。如果自己用Python代码写分组逻辑,性能肯定会打折扣,但用batched直接把分组工作交给底层C代码,速度快很多。

  • 内存访问的局部性优势
    分组合并的方式每次处理的是列表中相邻的元素对,Python列表的内存布局是连续的指针数组,相邻元素的指针在内存里也是靠近的,CPU缓存的命中率更高。虽然这个影响比前两个小,但也是性能提升的一个辅助因素。

  • 减少Python级别的变量更新次数
    test1每次循环都要更新num和den两个变量,这是Python层面的赋值操作,次数多了累积的开销也不小。而test2的计算是批量在列表推导式里完成的,变量更新是在C层面批量处理,减少了Python级别的赋值次数。

核心结论

两种方式的数学计算量完全一致,但test2把大部分循环和计算工作交给了Python的底层C实现,而test1的核心逻辑全在Python层面的循环里——这就是性能差异的关键!毕竟Python解释器的循环开销比C代码高太多,能把计算逻辑“下沉”到C层面的部分越多,性能就越好。

不知道大家有没有其他的见解?或者有没有遗漏的性能影响因素?欢迎一起讨论~

火山引擎 最新活动