请在2024年Clojure现状调查!中分享您的想法。

欢迎!请参阅关于页面以获取关于如何使用本页面的更多信息。

0投票
编译器

结果是Clojure为每个函数创建一个类文件。因此,你得到了太多的.class文件。

我的问题是,为什么不按命名空间创建一个类文件,而是将所有函数都放入该.class文件中,而是生成那么多的.class文件呢?

至少对于AOT编译,将这些.class文件合并在一起会好得多?我想知道背后的技术原因。

3个答案

+2投票

被选中 通过
 
最佳答案

Clojure的编译器不是一个整程序编译器。编译的单位是顶级形式。所以当你说“编译这个文件”时,实际上要编译该文件中的每个顶级形式,编译器一次只能“看到”一个顶级形式。

函数是闭包,因此它们不仅仅是代码(像静态方法一样),它们是代码+封闭的值(像实例方法,其中的封闭值是实例的字段)。

例如

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

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

使用静态方法实现函数还有另一个问题,即函数是一级值,而静态方法则不是。你无法将静态方法作为参数传递,等等。可用反射的Method和indy的MethodHandles提供一些静态方法的一级表示,但反射被认为是较慢的,并且MethodHandles在clojure最初创建时是不可用的。在任何情况下,这两种表示都无法实现clojure的函数接口IFn,因此需要大量工作才能使其工作(可能需要重新设计clojure调用函数的方式以使用invoke动态)。

在一般情况下,函数需要能够封装值(实例字段),并且需要有一种一级表示(实例),因此静态方法不可取。

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

我一直致力于研究针对https://clojure.atlassian.net/browse/CLJ-701的方法(我尚未发布任何补丁,它仍然不起作用),这会导致编译器更积极地用静态方法替换某些函数定义和调用,并且完全消除了为这些函数生成单独类的需求,但它范围极广,并且仅限于在其中立即调用的函数,它们的定义没有逃离。这不会涵盖顶级定义,因为def使函数的值成为全局的,这有点像逃离的定义。

by
感谢提供这些信息!
0投票
by

我认为其中一个问题是,在进行热加载时,你希望能够逐个加载或重新加载单个函数,但在Java中,你无法在现有类上添加或更新方法,但你可以在类级别上这样做,因此通过为每个函数创建一个类,我们可以逐个加载和重新加载一个函数,而不是一次只加载一个命名空间。

现在,我也记得阅读过在某种程度上,它是如何制作的,并从这里演变而来的。我认为也许现在可能会有其他方法来做这件事情,可能使用invoked dynamic和method handles,尽管这超出了我的理解,但如果新的JDK现在提供这种选项,那么需要重写很多东西来改变它,这是不现实的。

我知道这些,但我无法确认这一点。

0投票
by

我对您看到的‘产生过多的类文件’的具体副作用感到好奇?更大的JAR文件、长的require时间等?

您正在寻求优化大量类文件中的哪个方面?

有多少个类文件算是太多?

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