dreamlike-ocean / StableValue

使用invokedynamic实现的零依赖StableValue用于取代DCL模式
8 stars 0 forks source link

讨论一下StableValue #1

Open liach opened 4 months ago

liach commented 4 months ago

看到作者在twi上介绍这个库的起因是看到Per Minborg说要研究StableValue(JDK自带的@Stable用API发布),暂时分享下看这个库的感想:(很多词不知道中文术语,将就看看吧) 现在公开能当stable用的功能基本都和class绑定,主要是<clinit>(Holder class)、indy(Lambda)、condy三种模式。这个仓库用indy模式再加一层class。 1、我个人设计API会设计个 MethodHandle initializer->MethodHandle memoized 模式,这样相当于动态Holder class,memoized指向Holder class static final field,Holder class 会呼叫initializer。 2、为了使用方便,API设计(比如这个项目)一般都实现一个接口。但是实现接口的问题是要创建实例,然后实例要调用stable功能就要专门用一个class,比如一个实现class自带indy/condy创建一次实例。这个为了支持接口相当于多一层class皮,但是这层皮里面能用condy indy还算不错。我个人倾向写condy,ConstantBootstraps.invoke + MethodHandle(Supplier.get) + MethodHandles.classData,定义时传入supplier当classdata,比这种用static field更安全。

这两个的最大问题都是每一个StableValue都要定义一个class,但是class定义多了吃内存,而且如果一个interface有太多实现,也会有profile pollution,尤其是放list里面。

相比之下,@Stable几乎没有任何成本,只需要字段额外占用一个全零值,同时赋值也更灵活。StableValue相比之下封装就很厚了。因为Java设计理念是下限比较高,然后stable玩坏的后果也比正常field玩坏的后果大,所以封装比较复杂,再写了一套dcl封装;但是还是比额外定义class成本更低,而且它基础的get+setIfUnset可以替代现有的atomicreference get+compareAndExchange,效率更高。

然后关于classfile api用途,一般创建classdesc是clazz.describeConstable().orElseThrow(),最好存static final,或者直接拿ConstantDescs里面的用;然后TypeKind有TypeKind.from(TypeDescriptor.OfField)可以直接接收Class。这样写起来会方便很多。

还是很喜欢作者这个实验项目的,因为能了解用户对classfile api和stable value的看法,对提升这两个API的用户体验有帮助。

dreamlike-ocean commented 4 months ago

感谢回复,实际上的实现确实是特化了一个sam的实现类以求使用indy进行常量折叠,也考虑到了类加载的问题所以尝试添加了一个hiddenclass的方案进行实现。 关于classdata的部分我也仔细看过Methodhandle.classdata但是有些语焉不详,说来惭愧,我其实并不了解classdata的作用,如果您有对应的参考资料可以提供,我将不胜感激。 classfile api部分api确实有点太多了而且jep也没有详细的使用说明,摸索了一些api进行实现这个项目,同样感谢您提供的建议,我会好好研究下。 最后也期待StableValue正式合入主线,让@Stable注解更方便地被使用

liach commented 4 months ago

hidden class介绍的确难找。它定义主要写在MethodHandles.Lookup#defineHiddenClass了,裹脚布一样长。它最早是和java.lang.invoke一起添加的Unsafe.defineAnonymousClass,叫VM匿名类(和Java的匿名类完全无关),用来存储实现LambdaForm生成的字节码。

它主要两个特点,一个是name和字节码不一样,Class#getName会返回带/,所以不能在字节码里面表达(但是这个类本身可以通过thisClass表达自己),同时hidden interface不能被实现。Class.describeConstable()返回Optional,返回Optional.empty的情况就是这个class是hidden class,或者是hidden class组成的数组。同理,java.lang.invoke也不能用字节码指向hidden class,但是它有MemberName绕开此限制,所以可以配合MethodHandle.linkToXxx呼叫hidden class的static method、constructor等;它也是用Unsafe绕开字节码对field的限制。这些就是MethodHandle和LambdaForm实现的底层逻辑。

它定义时可以定义是否允许当NESTMATE,就是使用定义它的Lookup的lookupClass的nest里面的private成员,这个对lambda很有用;然后STRONG是定义这个hidden class可以不可以和lookupClass的类加载器垃圾回收分开,不定义的话允许提早卸载hidden class,但是会增加元数据,所以lambda类一般定义是STRONG,附加影响是LambdaMetafactory实际上并不适合非编译器使用,因为它生成的lambda会和lookupClass.getClassLoader绑定难以垃圾回收,生成太多会炸内存。

ClassData最大的用处是在定义class时候用线程安全的方法传入预处理的数据,避免生成class里再重复计算,或者使用各种麻烦或者线程不安全的形式传输数据。它也是VM anonymous class一开始就有的功能,主要是用来传入对其他VM anonymous class的指针。(最早实现是常量池打补丁)同时也用来传入对其他MethodHandle和LambdaForm的常量指针,用于字节码编译过的LambdaForm;InvokerBytecodeGenerator。同样lambda实际上也会用classdata储存MethodHandle指针,主要针对违反字节码规则的一些情况还有最近添加的方便指向hidden class里面的静态方法(类似MethodHandle实现了)

用户代码里面classData一般用defineHiddenClassWithClassData传入,读取是用MethodHandles.classData传入一个"original lookup",比如在hidden class里面呼叫MethodHandles.lookup(),或呼叫bootstrap method提供的,或者defineHiddenClassWithClassData返回的;IMPL_LOOKUP.privateLookupIn不能用!然后这个API参数刚好是和condy bootstrap method形式一样,所以可以直接写字节码写个呼叫classData的condy,然后以后用classData直接呼叫condy。或者也可以正常写java代码里面直接用classData,但是加载class时直接读取byte传入defineHiddenClassWithClassData,也是可行的。

ClassFile API如果是专门生成类,推荐看MethodHandleProxiesProxyGenerator,恰好都是生成实现interface的类的。

我感觉也许可以写点markdown文章介绍介绍ClassFile API,毕竟是从minecraft字节码那边过来的,对asm比较熟悉,也可以介绍下和asm的差别等。

最后我也特别希望stable value能上线,能让JIT优化范围大幅扩大。我也在那个PR里看了一圈,对代码和API提了些建议。虽然为了安全起见,代码有点像dcl了,但是JIT优化后生成的编码会直接简化成读取一个常量值,而且API包含computeIfUnset的话也对用户比较友好。现在主要麻烦是实例final字段不能constant fold,不过话说回来,hidden class的final字段可以constant fold,也是一个有趣的特性;同时valhalla也在考虑支持呼叫super constructor前给安全final字段赋值,可能有所帮助。不过你也知道Java下限比较高,FFM API也是有了稳定的安全的Arena后才结束preview,stable value上线应该也会细调API避免性能瓶颈,比如现在的模型里,computeIfUnset里面传入capturing lambda性能就会大幅下降;感觉这些问题解决后stable value才能结束preview。

dreamlike-ocean commented 4 months ago

实操了下condy作为生成后端实现,确实比indy更适合一些,感谢建议 114809a7fe8b10fca1a54235afa065e2abd98176

liach commented 4 months ago

顺便评论下DirectLambdaFactory,实际上JDK从7开始自带MethodHandleProxies.asInterfaceInstance实现接口,但是因为性能问题,后来用的LambdaMetafactory不是直接用MethodHandle而是把MethodHandle crack后直接变字节码,所以不饿能用这种hidden class MethodHandle。你这个DLF有个小问题,是adjustMethodHandle不知道为什么来个type=type.erase()所以你这个没法实现有int long float double Object之外类的参数和返回的接口

dreamlike-ocean commented 4 months ago

DirectLambdaFactory的初衷在于Lambdafactory并不能接受非directmethodhandle,比如说Methodhandles::bindTo转换过的mh,所以想做个代理来调用,里面实现的比较糙 后面尝试了下MethodHandleProxies 其实感觉可以满足我的需求了