2024年Clojure状态调查!中分享您的想法。

欢迎!请参阅关于页面以获取更多关于此工作方式的信息。

+3
编译器

看起来编译器生成的字节码在所有输入(工具、依赖、编译器选项、代码)保持稳定的情况下并非100%确定。可以通过以下脚本观察到这一点,该脚本只是循环编译相同的代码,直到两次连续运行的结果不同

#!/usr/bin/env bash

set -euo pipefail

compile() {
    mkdir -p classes/curr

    clojure -Sdeps '{:path ["src" "classes/curr"]}' \
            -M -e "(binding [*compile-path* \"classes/curr\"] (compile 'foo) nil)"

    if [ -d "classes/prev" ]; then
        diff <(cd "classes/prev" && sha256sum * | sort -k2) \
             <(cd "classes/curr" && sha256sum * | sort -k2)
    fi
}

run() {
    rm -rf classes/prev classes/curr
    compile
    local n=1
    while compile; do
        echo $n
        rm -rf classes/prev
        mv classes/curr classes/prev
        n=$(($n+1))
    done
}

run

其中 src/foo.clj 包含以下代码(改编自我在 Aleph 中遇到此问题的某个实际代码)

(ns foo)

(defn bar []
  (let [a 1
        b 2
        c (delay 3)
        {:keys [foo bar baz qux bla frob]} {:foo "ha"
                                            :bar 4}]
    #(clojure.lang.ArraySeq/create (into-array [a b @c bar]))))

我在 OpenJDK 11.0.15+10 和 Clojure CLI 1.11.1.1149(即 Clojure 1.11.1)上的 Linux 5.15.59 上运行了这个脚本。经过几十次迭代后,结果是这种样子

2,3c2,3
< 57496515c08ffd087a1f3e3e0d6e420c291b27a15d883c93cdad5de1c2cd8bf6  foo$bar$fn__145.class
< f6d5832ee0ee590056911b70da99b525d93d5b0280feb8e9d34e2f214de5dedd  foo$bar.class
---
> eff4dac36986b909c7dacb63b87f2033dc5be62ee5e0b2a3a2a4207e79a77c41  foo$bar$fn__145.class
> 3a9b9f33e4eeed8b80810a02dbeb0e4d72fac83d496409e5cf7c6ff78fa36ff5  foo$bar.class

像这样比较反汇编的类文件

$ diff -u <(javap -l -c -s -private classes/prev/foo\$bar\$fn__145.class) <(javap -l -c -s -private classes/curr/foo\$bar\$fn__145.class)

结果是

@@ -3,23 +3,23 @@
   java.lang.Object c;
     descriptor: Ljava/lang/Object;
 
-  long a;
-    descriptor: J
-
   long b;
     descriptor: J
 
   java.lang.Object bar;
     descriptor: Ljava/lang/Object;
 
+  long a;
+    descriptor: J
+
   public static final clojure.lang.Var const__0;
     descriptor: Lclojure/lang/Var;
 
   public static final clojure.lang.Var const__1;
     descriptor: Lclojure/lang/Var;
 
-  public foo$bar$fn__145(java.lang.Object, long, long, java.lang.Object);
-    descriptor: (Ljava/lang/Object;JJLjava/lang/Object;)V
+  public foo$bar$fn__145(java.lang.Object, long, java.lang.Object, long);
+    descriptor: (Ljava/lang/Object;JLjava/lang/Object;J)V
     Code:
        0: aload_0
        1: invokespecial #16                 // Method clojure/lang/AFunction."<init>":()V
@@ -28,13 +28,13 @@
        6: putfield      #18                 // Field c:Ljava/lang/Object;
        9: aload_0
       10: lload_2
-      11: putfield      #20                 // Field a:J
+      11: putfield      #20                 // Field b:J
       14: aload_0
-      15: lload         4
-      17: putfield      #22                 // Field b:J
+      15: aload         4
+      17: putfield      #22                 // Field bar:Ljava/lang/Object;
       20: aload_0
-      21: aload         6
-      23: putfield      #24                 // Field bar:Ljava/lang/Object;
+      21: lload         5
+      23: putfield      #24                 // Field a:J
       26: return
     LineNumberTable:
       line 4: 0
@@ -46,10 +46,10 @@
        3: invokevirtual #35                 // Method clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
        6: checkcast     #37                 // class clojure/lang/IFn
        9: aload_0
-      10: getfield      #20                 // Field a:J
+      10: getfield      #24                 // Field a:J
       13: invokestatic  #43                 // Method clojure/lang/Numbers.num:(J)Ljava/lang/Number;
       16: aload_0
-      17: getfield      #22                 // Field b:J
+      17: getfield      #20                 // Field b:J
       20: invokestatic  #43                 // Method clojure/lang/Numbers.num:(J)Ljava/lang/Number;
       23: getstatic     #46                 // Field const__1:Lclojure/lang/Var;
       26: invokevirtual #35                 // Method clojure/lang/Var.getRawRoot:()Ljava/lang/Object;
@@ -58,7 +58,7 @@
       33: getfield      #18                 // Field c:Ljava/lang/Object;
       36: invokeinterface #49,  2           // InterfaceMethod clojure/lang/IFn.invoke:(Ljava/lang/Object;)Ljava/lang/Object;
       41: aload_0
-      42: getfield      #24                 // Field bar:Ljava/lang/Object;
+      42: getfield      #22                 // Field bar:Ljava/lang/Object;
       45: invokestatic  #55                 // Method clojure/lang/Tuple.create:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/IPersistentVector;
       48: invokeinterface #49,  2           // InterfaceMethod clojure/lang/IFn.invoke:(Ljava/lang/Object;)Ljava/lang/Object;
       53: checkcast     #57                 // class "[Ljava/lang/Object;"

如图所示,闭包局部变量的顺序似乎随机变化。当系统负载较重时(例如,在同时运行 Clojure 的完整测试套件时),这种情况的发生几率会增加。这让我相信这是由内存分配引起的。实际上,在检查 Compiler.java 中的相关代码时,我认为我可能已经找到了罪魁祸首:`CLEAR_SITES` 映射使用 `LocalBinding` 实例作为键,但该类没有实现 determi...(未完待续)

public int hashCode(){
    return Util.hashCombine(idx, sym.hashCode());
}

尽管我还没有在任何地方读到过编译器对可重复构建的关注度,但鉴于其在其他方面看来相当可靠,而这个修正只需要进行很小改动就能使其更可靠,我认为提出这一点是有意义的。

1 个回答

0

这并不是我们高度重视的目标。但这并不意味着,如果它能解决某个实际问题,我们不会考虑做出改变。

谢谢您的快速回复!实事求是地说,这是我们推动的案例:目前,我们把一个相当大的应用程序部署到多个服务器上,形式是带有AOT编译类文件的uberjar。在我写下主要评论的时候(中间省去一些不常用的繁重传递依赖关系,减小到60M),这些文件大约90M。在开发某些特性时,从开发机器向测试集群部署更改很有帮助。理想情况下,以小的增量进行,以便进行紧密的反馈。然而,由于许多人现在在家工作,并且上行的带宽往往有限,这种传输可能非常慢。这就是我们为什么要寻找加快这一过程的方法。我们研究的一种方法就是直接部署AOT(提前编译)的类文件,即不考虑Uberjar。这样,部署机制就能根据服务器已有的内容(比如`rsync -c`)仅传输文件。现在实现这个目标最简单的方法就是像以前一样编译整个应用程序(即在本地重新使用以前构建的结果)。通常这应该与前一个构建相比相对较小,因为例如依赖关系(构成了负载的大部分)很少改变。但由于提到的非确定性,我们仍然做了比必要的更多更改。

BTW:我明白存在达到上述目标的可行替代方案。只是有额外的考虑让我们先研究这个方法::-)
...