💡 过早优化是万恶之源。 ——Tony Hoare
作为软件开发人员的一句名言,相信绝大多数小伙伴都有听闻过这句名言,而我在最近阅读netty源码的时候就见识了这么一个有趣的例子。
Netty是一个用于构建高性能、可伸缩的网络应用程序的异步事件驱动框架。它主要关注在网络通信、协议处理和高性能的特性上,是一个基于Java的开源框架。Netty的设计目标是提供简单而强大的 API,使得开发者能够轻松地构建各种网络应用,包括但不限于服务器和客户端。
可能很多开发同学没有直接使用过netty,但它作为一个优秀的通信框架你很有可能在实际项目中已经在使用了,比如国内流行的rpc框架dubbo底层默认就使用netty作为通信层,此外还有Elasticsearch、RocketMQ、Camel等等中间和框架。
如此优秀的框架非常值得一读,而我也在阅读FastThreadLocal中发现了一个有趣的优化。
在 Netty 中,FastThreadLocal 是一种优化过的线程本地存储(ThreadLocal)实现,用于提供更高性能的线程本地变量访问。它的设计目标是减小线程本地存储的性能开销,特别适用于高并发的网络应用场景,如 Netty 所涉及的网络通信框架。
作为其核心原理的InternalThreadLocalMap
内有这样一行
// Cache line padding (must be public)
// With CompressedOops enabled, an instance of this class should occupy at least 128 bytes.
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8, rp9;
故事今天就是围绕这一行展开的,众所周知基于时间局部性和空间局部性原理所以当处理器需要加载内存中的数据时,它会加载整个缓存行,而不仅仅是请求的特定数据,缓存行的大小通常是2的幂,例如64字节。当一个线程修改缓存行中的数据时,整个缓存行都会被标记为"脏",这会导致其他线程中缓存行的数据无效,需要重新加载,这被称为伪共享。
为了避免伪共享和优化多线程程序的性能,可以使用以下方法:
-
使用
@Contended
注解(仅在Java 8及更新版本中可用):该注解可用于类或字段上,可以告诉JVM在生成字节码时添加填充以避免伪共享,原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突,具体可以参考
RFR (S): JEP-142: Reduce Cache Contention on Specified Fields
- 缓存行填充(Cache Line Padding):通过在数据结构的末尾添加一些无关的变量,使得不同线程操作的数据不在同一个缓存行上。这样可以减少伪共享的影响。
而很明显上面的代码就属于第二种,试图通过行填充解决伪共享,看上去好像没什么问题,netty使用各种小技巧的地方也非常多,但这一行在https://github.com/raidyue/netty/commit/ef540815a98dac50769e38b39e5107dc5a313b47 中被改为了
/** @deprecated These padding fields will be removed in the future. */
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8, rp9;
哎哎哎,刚才还说这一行多么老道,这会怎么就要删除了,提交的log大概是这么说的
我没有看到填充有任何明显的优势。
唯一受保护的其他字段是对 BitSet 的很少更改的对象引用。
填充也使用“long”,这不一定会阻止 JVM 将上述对象引用放入对齐间隙中。
好嘛,原来只是一个拍脑袋的优化,也并没有基准测试的数据。
但代码已经在了删除的它的风险就很高,所以只是写了一行注释要删除却没有真正的删除它。
甚至于还有后续,在https://github.com/netty/netty/pull/12309 中它被改成了这样
/** @deprecated These padding fields will be removed in the future. */
public long rp1, rp2, rp3, rp4, rp5, rp6, rp7, rp8;
细心的小伙伴发现少了一个rp9,因为在之前的某次功能合并中InternalThreadLocalMap
增加了一个字段
private ArrayList<Object> arrayList;
导致缓存行填充从128字节破坏为136字节。。。
为此又做了一个修复去掉了一个long field,黑魔法用的多确实容易被反噬。,这就是一个活生生的例子。
甚至于最近还有人提到这个事情https://github.com/netty/netty/issues/12312 有开发者认为如果是为了避免伪共享,那么随着jdk内存布局的调整我们应该使用字节字段进行填充而不是long去做填充。netty的作者估计也是不想在这个事情上多做纠缠,明确在主干分支上已经把这个玩意干掉了,4.x版本后续也会逐步移除它,没有更严格的基准测试之前我们将不会再做填充。
看完这个系列希望各位同学们慎用黑魔法,放下脑子一热的优化,多去借鉴借鉴他人优秀的架构与设计 😎写出更优雅的代码。