一、问题往往不是出在你以为的地方
系统变慢的时候,大多数人的第一反应都很一致:是不是SQL写得不够好,是不是哪里没加缓存,是不是算法可以再优化一下。
然后开始改查询、加索引、做缓存,甚至加机器。短时间内可能确实有效,但过一段时间,问题又会回来,而且通常比之前更严重。
这类问题有个共同点:你每次都在“修表象”,但真正的原因一直没动。
在.NET体系里,代码执行并不是直接发生的,中间隔着一整套运行时机制——GC、线程池、JIT/AOT、内存分配。这些东西平时你几乎感觉不到,但一旦规模上来,它们就会成为主要成本来源。
所以很多时候,并不是代码慢,而是系统为了执行这些代码,付出的代价越来越高。
二、真正拖慢系统的,通常是“分配”
如果你看.NET这几年的演进方向,会发现一个很明显的趋势:一直在减少分配、减少内存占用、减少运行时参与。比如AOT、Trimming、Span这些东西,本质都在做一件事——降低运行成本。
这背后其实说明了一件很现实的事情:在大多数业务系统里,计算本身并不贵,贵的是“围绕计算产生的开销”。
最常见的就是对象分配。每一次new,都会进入托管堆,参与GC。当请求量上来之后,对象数量会迅速膨胀,GC开始频繁工作。GC一旦频繁触发,CPU时间就会被大量消耗在扫描、移动对象和暂停线程上。
这时候你看到的是接口变慢,但实际发生的是:系统在忙着回收你刚刚创建的那些对象。
还有一些更隐蔽的情况,比如LINQ链式调用、Lambda闭包、字符串拼接、装箱拆箱。这些写法本身没有问题,但它们在背后会不断制造临时对象。在低并发下几乎感觉不到,但在高并发场景里,这些“微小成本”会被放大成真实瓶颈。
很多人优化代码时完全没意识到这一点,还在不断引入新的分配点,最后变成一个典型现象:代码越来越“优雅”,系统却越来越慢。
三、分层没有错,但很多系统已经分“过头”了
Controller、Service、Repository这一套,本来是为了让代码更清晰、更易维护。但在很多项目里,它已经变成了一种惯性:不管业务复杂与否,先分三层再说。
问题在于,每一层都不是免费的。一次请求进来,可能要经过多次方法调用、对象转换、序列化处理,最后才真正触达到数据库。单看每一步都没什么问题,但叠加起来之后,请求路径会变得很长,而真正“做业务”的部分反而只占很小一段。
更麻烦的是,这些层之间往往还伴随着额外的抽象,比如DTO转换、接口封装、动态代理。这些设计在代码层面看起来很规范,但在执行层面,其实是在不断增加系统负担。
你会看到一种现象:业务很简单,但调用链很复杂;逻辑没多少,但耗时却不低。这类问题,很少能通过“优化某一层代码”解决,因为问题本身就不在某一层,而是在整体结构。
四、很多性能问题,其实是“无效工作”太多
如果把这些问题放在一起看,会发现一个共性:系统在做很多“没有必要的事情”。
为了抽象多走了几层调用,为了通用性引入反射,为了方便开发创建了大量短生命周期对象,为了代码优雅用了复杂的表达式。这些决策单独看都合理,但叠加起来之后,就变成了一种负担。
在过去,这种负担还能被JIT和运行时“部分消化”。但现在情况在变。随着AOT、Trimming、Source Generator这些能力越来越成熟,运行时正在逐步“退场”。很多过去可以在运行时动态处理的事情,现在必须在编译阶段就确定下来。这意味着,多余的抽象、多余的依赖、多余的代码,都会变成真实成本,而不是可以被隐藏的细节。
归根结底,系统的执行方式越来越“直接”,也越来越“诚实”。你写了多少,它就执行多少,不再帮你兜底。
在这种情况下,真正影响性能的,不再是某一段代码写得好不好,而是你整个系统到底做了多少无效工作。
五、优化的方向,其实很简单:让系统少做事
当你把问题看清楚之后,很多优化思路反而变得简单了。
减少不必要的对象创建,控制好数据结构和生命周期,让GC压力保持在可控范围;在读多写少的场景里,减少不必要的抽象,必要时直接打通数据访问路径;对高频接口,优先考虑执行路径,而不是代码“是否优雅”。
还有比较重要的一点是,不要把所有问题都交给运行时解决。过去你可以依赖它帮你兜底,但现在,这种兜底空间正在变小。
很多时候,真正有效的优化,不是把某段代码改得更复杂,而是反过来问一句:这段逻辑是不是可以不做,或者换一种更直接的方式完成。
结语
系统变慢,很少是因为某一行代码写错了,更多是因为整体成本在不断累积,而你没有察觉。.NET这几年的演进,其实一直在强调一件事:减少运行时参与,减少不确定性,让执行路径更简单、更直接。
如果顺着这个方向去看,你会发现很多问题的答案并不复杂——不是做得更聪明,而是做得更克制。