请在2024 Clojure状态调查!分享您的想法。

欢迎!有关更多信息,请参阅关于页面。

0投票
编译器

实际上,Clojure为每个函数创建一个类文件。因此,你会得到过多的.class文件。

我的问题是,为什么它会生成那么多.class文件,而不是在每个命名空间创建一个类并将所有函数放在那个.class文件中?

至少对于AOT编译,将那些.class文件合并是不是很好?我想知道背后的技术原因。

3个答案

+2投票

已选答案
 
最佳答案

Clojure的编译器不是一个完整的程序编译器。编译的单位是顶级形式。因此,当你编译某个文件时,实际上是该文件中的每个顶级形式都单独编译,编译器一次只能“看到”一个顶级形式。

函数是闭包,所以它们不仅是代码,还包含闭包的值(类似于实例方法,闭包的值是实例的字段)。

例如

(fn [x] (fn [y] [x y]))

此代码有一个内部和外部函数,并编译成两个类。每次调用外部函数时,都会创建内部函数类的新实例,当它构造时传递x的值,并将其存储在某些实例字段中。

使用静态方法来实现fn的问题在于,fn是第一类值,而静态方法不是。你不能将静态方法作为参数传递等。有一些静态方法的first class表示形式,例如反射的Method和indy的MethodHandles,但反射被认为是较慢的,并且MethodHandles在Clojure最初创建时并不可用。在两种情况下,这些表示都无法实现Clojure的函数接口IFn,因此需要大量工作才能使它们工作(可能需要重新设计Clojure调用函数的方式以使用invoke动态)。

在一般情况下,函数需要能够封闭值(实例字段),并需要某种first class表示(实例),因此静态方法是不可用的。

但是,Clojure编译器确实做一些优化,在可能的情况下为函数生成静态方法,在某些情况下将函数调用转换为静态方法调用。但是,它仍然生成实例方法版本(这仅调用静态方法),因为编译器并不能总是知道调用的是哪个静态函数,因此所有函数都必须支持相同的通用调用约定(作为一个实现IFn的实例)。

我正在致力于一种方法来处理https://clojure.atlassian.net/browse/CLJ-701的问题,我还没有发布任何补丁(它仍然无法工作),这使得编译器更积极地替换某些函数定义和调用,并将这些函数转换为静态方法,这完全消除了为这些函数生成单独类的要求,但这在范围上非常有限,并且只有立即在其定义之处调用并且定义不逃逸的函数才适用。这不会涵盖顶层def,因为def会使函数的值成为全局的,这就像逃逸的定义。

by
谢谢信息!
0投票
by

我认为一个问题是在热加载时,你应该能够一次重新加载或加载一个函数,但在Java中,你不能在现有类上添加或更新方法,但你可以在类级别上这样做,因此通过为每个函数提供一个类,我们可以一次加载和重新加载一个函数,而不仅仅是一次namespace。

现在,我也记得我读到某种程度上,它是这样的,并且它从那里发展而来。我认为现在可能还有其他方法可以这样做,也许可以使用invoked dynamic和method handles,尽管这超出了我的理解,但如果新的JDK现在提供了这个选项,那么需要重写很多东西来改变它,这并不现实。

我知道的就是这样,但我无法确认这一点。

0投票

我很想知道你看到了“类文件过多”的具体副作用是什么?较大的jar包,长加载时间等?

你想要优化大量类文件的哪个方面?

多少个类文件算是太多?

我正在尝试将 JVM 字节码生成 WASM 代码。所以,我需要转换 .class 文件。问题是,每个函数都创建自己的类,结果就是文件数量非常多(1k+,根据项目大小而变化),因此这变得很耗时,这就是我为什么试图了解每个函数生成类的理由。
...