一个脚本跑十分钟,你骂Python慢。换成C++重写,三天后跑八分钟。问题不在语言,在你没看见的那些细节。韩愈说“业精于勤,荒于嬉”,性能优化也一样,勤在细处,荒在粗心。真正拖慢代码的,往往不是算法,而是你习以为常的写法。
循环里的重复计算,像慢性失血
for i in range(len(data)),每次迭代都调一次len()?len()是O(1),但属性查找和函数调用有开销。更隐蔽的是循环里访问全局变量、模块属性。Python的局部变量查找比全局快数倍。把len(data)、self.value、math.sqrt提到循环外,速度立刻不同。微小的开销乘以百万次,就是灾难。老子说“天下大事,必作于细”,循环里的每一纳秒,都在偷走你的时间。这不是过早优化,这是基本尊重——尊重解释器的工作原理。
列表还是生成器,别一刀切
生成器省内存,但未必更快。sum(x for x in data)比sum([x for x in data])省内存,可列表推导式在C层执行,有时反而更快。数据量小,列表推导式胜出;数据量大到内存吃紧,生成器才是救星。工具没有优劣,场景决定一切。谚语说“杀鸡焉用牛刀”,反过来,宰牛也不能用水果刀。盲目用生成器,可能换来更慢的迭代和更复杂的调试。先测量,再选择。
字符串拼接,+号是温柔陷阱
循环里s += str(i),每次创建新字符串,旧字符串被丢弃。百万次拼接,内存分配和复制让你怀疑人生。''.join(parts)一次性分配,快得不是一星半点。更大量用io.StringIO。字符串不可变,每次加号都在重建世界。这道理像滚雪球,越滚越大,最后压垮性能。别让拼接成为你的瓶颈,用join,用f-string,用格式化,别用加号堆长城。
内置函数是C写的,别用Python重造轮子
sum、min、max、any、all、map、filter,这些内置函数在C层循环,比你手写for快得多。有人用for累加,有人用reduce,可sum才是最短路径。你写的Python循环,每步都在和解释器握手;内置函数直接跑在C里,连招呼都不打。《论语》说“工欲善其事,必先利其器”,内置函数就是现成的利器。别为了炫技写复杂逻辑,简单直接往往最快。
缓存是时间的复利
递归算斐波那契,不缓存就是指数爆炸。functools.lru_cache一行装饰器,把重复计算变成字典查找。动态规划的本质就是缓存,可很多人只在面试时想起。缓存不是优化,是常识。同样的参数反复调用,纯函数就该记住结果。巴菲特说复利是世界第八大奇迹,缓存在代码里就是复利的化身。但注意,带副作用的函数别乱缓存,否则脏数据让你哭都来不及。
GIL不是借口,选对并发模型
CPU密集任务,多线程被GIL锁死,换多进程。IO密集任务,异步或多线程都能打。可有人不管什么场景,上来就threading,结果比单线程还慢。GIL是限制,不是判决书。进程间通信有成本,异步代码有学习曲线。选型前问自己:瓶颈在CPU还是在等待?答案不同,工具不同。谚语说“一把钥匙开一把锁”,别拿锤子看什么都像钉子。
数据结构选错,代码跑断腿
在列表里查一个元素,O(n);在集合或字典里查,O(1)。一万条数据,列表遍历几毫秒,集合几乎瞬间。collections模块的deque、defaultdict、Counter,都是为特定场景造的轮子。选错容器,算法再优雅也白搭。有人用列表当队列,pop(0)是O(n),deque.popleft()是O(1)。细节决定成败,容器决定快慢。
对象创建与__slots__,内存的隐形税
每个实例默认有__dict__,字典开销大。定义__slots__,实例变成紧凑数组,内存省一半,访问还更快。百万个对象,差异就是几百兆。看不见的内存,看得见的账单。但别滥用,__slots__牺牲了动态属性。权衡之后,数据类、命名元组、attrs都是选项。性能不是抠门,是清醒。
性能优化不是玄学,是习惯。你忽略的每个细节,都在暗中标好了价格。不积跬步,无以至千里;不省微秒,何以谈高效。从今天起,把循环里的重复拿出去,把该缓存的缓存,把该换的容器换掉。Python慢不慢,取决于你写得多细。