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

欢迎!请参阅关于页面以了解更多此平台的工作方式。

0
Java 互操作

我偶然发现了性能基准测试在这里,我觉得很奇怪为什么 Clojure 的执行速度会被 Java 打败。

所以我将它扔进了分析器(在修改他们版本使用非检查数学后 - 这并没有帮助)但是没有显示出来。嗯。反编译并发现

// Decompiling class: leibniz$calc_pi_leibniz
import clojure.lang.*;

public final class leibniz$calc_pi_leibniz extends AFunction implements LD
{
    public static double invokeStatic(final long rounds) {
        final long end = 2L + rounds;
        long i = 2L;
        double x = 1.0;
        double pi = 1.0;
        while (i != end) {
            final double x2 = -x;
            final long n = i + 1L;
            final double n2 = x2;
            pi += Numbers.divide(x2, 2L * i - 1L);
            x = n2;
            i = n;
        }
        return Numbers.unchecked_multiply(4L, pi);
    }

    @Override
    public Object invoke(final Object o) {
        return invokeStatic(RT.uncheckedLongCast(o));
    }

    @Override
    public final double invokePrim(final long rounds) {
        return invokeStatic(rounds);
    }
}

所以看起来整数/长类型边界至少花费我们一个方法查找,可能在 Numbers.divide 中?
所以我就将所有内容强制转换为双精度浮点数(甚至我们的索引变量)

(def rounds 100000000)

(defn calc-pi-leibniz2
  "Eliminate mixing of long/double to avoid clojure.numbers invocations."
  ^double
  [^long rounds]
  (let [end (+ 2.0 rounds)]
    (loop [i 2.0 x 1.0 pi 1.0]
      (if (= i end)
        (* 4.0 pi)
        (let [x (- x)]
          (recur (inc i) x (+ pi (/ x (dec (* 2 i))))))))))
leibniz=> (c/quick-bench (calc-pi-leibniz rounds))
Evaluation count : 6 in 6 samples of 1 calls.
             Execution time mean : 575.352216 ms
    Execution time std-deviation : 10.070268 ms
   Execution time lower quantile : 566.210399 ms ( 2.5%)
   Execution time upper quantile : 588.772187 ms (97.5%)
                   Overhead used : 1.884700 ns
nil
leibniz=> (c/quick-bench (calc-pi-leibniz2 rounds))
Evaluation count : 6 in 6 samples of 1 calls.
             Execution time mean : 158.509049 ms
    Execution time std-deviation : 759.113165 ╡s
   Execution time lower quantile : 157.234899 ms ( 2.5%)
   Execution time upper quantile : 159.205374 ms (97.5%)
                   Overhead used : 1.884700 ns
nil

有什么想法吗?为什么Java 实现在除法时分担相同的惩罚?[两个版本都是使用 unchecked-math 实现,并在 :warn-on-boxed 模式下被实现]。

我还尝试了一个使用 fastmath 原始数学运算符的变体,实际上变得更慢了。到目前为止,没有任何方法能打败将循环索引 i 强制转换为双精度浮点数(这通常是我不会做的事情)。

在我进行基准测试时,这给出了与您的双精度解相同的功能,而无需将索引转换为双精度浮点数。

(defn calc-pi-leibniz3
  "通过解除长和双精度数字的混合以避免调用 clojure.numbers。"
  ^double
  [^long rounds]
  (let [end (+ 2 rounds)]
    (loop [i 2 x 1.0 pi 1.0]
      (if (= i end)
        (* 4.0 pi)
        (let [x (- x)]
          (recur (inc i) x (+ pi (/ x (double (unchecked-dec-int (unchecked-multiply-int (unchecked-int 2) (unchecked-int i))))))))))))

这是关于理解每个编译器插入的转换指令的位置


这是 Java 解决方案的字节码

  public static double go(int);
    descriptor: (I)D
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    代码
      堆栈大小=6,局部变量大小=6,参数大小=1
         0: 将常量1入栈
         1: 将索引1的值存入栈中
         2: 将常量1入栈
         3: 将索引3的值存入栈中
         4: 在常量池中加载整型4
         5: 将索引6的值存入栈中
         7: 从栈中加载索引6的值
        9: 从局部变量表中加载数值索引0的值
        10: 在常量池中加载整型2
        11: 相加整数
        12: 如果整型比较结果大于或等于常量,跳转到位置39
        15: 从栈中加载索引3的值
        16: 从常量池中加载double -1.0d
        19: 相乘double
        20: 将索引3的值存入栈中
        21: 从栈中加载索引1的值
        22: 从栈中加载索引3的值
        23: 在常量池中加载整型2
        24: 从栈中加载索引6的值
        26: 相乘整数
        27: 在常量池中加载整型1
        28: 相减整数
        29: 将整型转换为double
        30: 相除double
        31: 相加double
        32: 将索引1的值存入栈中
        33: 将索引5的值加1
        36: 跳转到索引7
        39: 从栈中加载索引1的值
        40: 从常量池中加载double 4.0d
        43: 相乘double
        44: 复制栈顶值到栈顶两个位置
        45: 将索引1的值存入栈中
        46: 返回double值


以下是你的解决方案

    public static double invokeStatic(long rounds);
        标志:PUBLIC, STATIC
        代码
               0: 从常量池中加载double 2.0
               3: 从局部变量表中加载长整型索引0的值/* rounds */
               4: 调用静态方法 clojure/lang/Numbers.add:(DJ)D
               7: 将索引2的值存入栈中/* end */
               8: 从常量池中加载double 2.0
              11: 将索引2的值存入局部变量索引1
              13: 将常量1入栈
              14: 将索引1的值存入局部变量索引x
              16: 将常量1入栈
              17: 将索引1的值存入局部变量索引pi
              19: 从局部变量表中加载double索引i
              21: 从栈中加载索引2的值/* end */
              22: double比较
              23: 如果不等于,跳转到索引36
              26: 从常量池中加载double 4.0
              29: 从局部变量表中加载double索引pi
              31: 相乘double
              32: 跳转到索引72
              35: 抛出异常
             36: 从局部变量表中加载double索引x
              38: 负数double值
              39: 将索引x的值存入局部变量索引x
              41: 下载           i
              43: 定义常量_1
              44: 双精度加法
              45: 下载           x
              47: 下载           pi
              49: 下载           x
              51: 将长整数加载到双精度浮点数中          2
              54: 下载           i
              56: 调用静态方法    clojure/lang/Numbers.multiply:(JD)D
              59: 定义常量_1
              60: 双精度减法
              61: 双精度除法
              62: 双精度加法
              63: 存储双精度浮点数          pi
              65: 存储双精度浮点数          x
              67: 存储双精度浮点数          i
              69: 跳转到            19
              72: 返回双精度浮点数


我的解决方案

    public static double invokeStatic(long rounds);
        标志:PUBLIC, STATIC
        代码
               0: 将长整数加载到双精度浮点数中          2
               3: 从局部变量表中加载长整型索引0的值/* rounds */
               4: 长整型加法
               5: 长整型存储到局部变量2         /* end */
               6: 将长整数加载到双精度浮点数中         2
              9: 长整型存储          i
              11: 定义常量_1
              12: 存储双精度浮点数          x
              14: 定义常量_1
              15: 存储双精度浮点数          pi
              17: 从局部引用加载长整型         i
              19: 从局部引用加载长整型         2 /* end */
              20: 长整型比较
              21: 如果不等于则跳转到            34
              24: 将双精度浮点数加载到双精度浮点栈         4.0
              27: 下载           pi
              29: 双精度浮点数乘法
              30: 跳转到            71
              33: 抛出异常
              34: 下载           x
              36: 双精度浮点数取反
              37: 存储双精度浮点数          x
              39: 从局部引用加载长整型         i
              41: 长整型常量_1
              42: 长整型加法
              43: 下载           x
              45: 下载           pi
              47: 下载           x
              49: 将长整数加载到双精度浮点数中         2
              53: 将局部变量 i 加载到操作数栈中
              55: 将长整型值转换为整型
              56: 整数乘法
              57: 向操作数栈中压入常量 1
              58: 整数相减
              59: 将整型值转换为双精度浮点型
              60: 双精度浮点型除法
              61: 双精度浮点型加法
              62: 将双精度浮点型值存储到池索引位置 pi
              64: 将双精度浮点型值存储到变量 x
              66: 将局部变量 i 加载到操作数栈中
              68: 转至指令编号 17
              71: 返回双精度浮点型值

同时也避免了所有的方法调用,直接与原始类型操作

尽管如此,在这个层面上,它仍然没有比你的解决方案表现得更好

根据我的基准测试,这两个解决方案的性能与 Java 那个解决方案相同
谢谢 Ben 的探讨。我觉得在 unchecked-math 的情况下,还需要进行手动类型转换,这似乎相当奇怪。另外,clojure.lang.Numbers 已经为除法情况提供了重载,应该会传递原始类型(人们会这么想的)。

很遗憾!
这是一个 Java 向量化版本,性能稍好一些 - (https://github.com/cnuernber/leibniz/blob/main/java/leibniz/JL.java :-)。

这里是关键 - 我不得不手动编写这个。整个 JVM 似乎缺少一个可以帮助处理这种工作的编译器。HotSpot 显然不会这么做。

还有一个实际问题,必要的 JVM 参数需要重复三次 - 一次在 deps.edn 中,一次在 build.clj 中,一次在执行系统的脚本中。如果在 build.clj 中的各种命令尊重任何在别名中找到的 jvm-opts 将会更好。

1 个回答

0

我可能需要一段时间才能查看这个问题,但是循环/递归边界容易陷入“救生圈”问题,这将导致显著的性能下降,但最好通过查看字节码来确认。

...