【问题标题】:How to get rid of clojure/lang/RT.aset and clojure/lang/RT.intCast in clojure array processing?如何摆脱clojure数组处理中的clojure/lang/RT.aset和clojure/lang/RT.intCast?
【发布时间】:2016-12-28 03:35:48
【问题描述】:

我尝试在 Clojure 中尽可能快地对复数数组进行乘法运算。

选择的数据结构是两个元素的映射,:re:im,每个元素都是 Java 原生数组,原始数组 double 用于低内存开销。

根据http://clojure.org/reference/java_interop,我对原始类型数组使用了精确的类型规范。

有了这些提示aget被转换成原生数组dload op,但是有两个效率低下,正是循环的计数器不是int而是long,所以每次索引一个数组的时候计数器通过调用clojure/lang/RT.intCast 转换为int。而且aset 不会转换为原生操作,而是转换为对clojure/lang/RT.aset 的调用。

另一个效率低下的地方是 checkcast。它检查每个循环,数组实际上是双精度数组。

结果是这段 Clojure 代码的运行时间比等效的 Java 代码多 30%(不包括启动时间)。能否在 Clojure 中重写此函数以使其运行更快?

Clojure 代码,要优化的函数是multiply-complex-arrays

(def size 65536)

(defn get-zero-complex-array
    []
    {:re (double-array size)
     :im (double-array size)})

(defn multiply-complex-arrays
    [a b]
    (let [
        a-re-array (doubles (get a :re))
        a-im-array (doubles (get a :im))
        b-re-array (doubles (get b :re))
        b-im-array (doubles (get b :im))
        res-re-array (double-array size)
        res-im-array (double-array size)
        ]
        (loop [i (int 0) size (int size)]
            (if (< i size)
                (let [
                    a-re (aget a-re-array i)
                    a-im (aget a-im-array i)
                    b-re (aget b-re-array i)
                    b-im (aget b-im-array i)
                    ]
                    (aset res-re-array i (- (* a-re b-re) (* a-im b-im)))
                    (aset res-im-array i (+ (* a-re b-im) (* b-re a-im)))
                    (recur (unchecked-inc i) size))
                {:re res-re-array :im res-im-array}))))

(let [
    res (loop [i (int 0) a (get-zero-complex-array)]
            (if (< i 30000)
                (recur (inc i) (multiply-complex-arrays a a))
                a))
    ]
    (println (aget (get res :re) 0)))

multiply-complex-arrays的主循环生成的java程序集是

  91: lload         8
  93: lload         10
  95: lcmp
  96: ifge          216
  99: aload_2
 100: checkcast     #51                 // class "[D"
 103: lload         8
 105: invokestatic  #46                 // Method clojure/lang/RT.intCast:(J)I
 108: daload
 109: dstore        12
 111: aload_3
 112: checkcast     #51                 // class "[D"
 115: lload         8
 117: invokestatic  #46                 // Method clojure/lang/RT.intCast:(J)I
 120: daload
 121: dstore        14
 123: aload         4
 125: checkcast     #51                 // class "[D"
 128: lload         8
 130: invokestatic  #46                 // Method clojure/lang/RT.intCast:(J)I
 133: daload
 134: dstore        16
 136: aload         5
 138: checkcast     #51                 // class "[D"
 141: lload         8
 143: invokestatic  #46                 // Method clojure/lang/RT.intCast:(J)I
 146: daload
 147: dstore        18
 149: aload         6
 151: checkcast     #51                 // class "[D"
 154: lload         8
 156: invokestatic  #46                 // Method clojure/lang/RT.intCast:(J)I
 159: dload         12
 161: dload         16
 163: dmul
 164: dload         14
 166: dload         18
 168: dmul
 169: dsub
 170: invokestatic  #55                 // Method clojure/lang/RT.aset:([DID)D
 173: pop2
 174: aload         7
 176: checkcast     #51                 // class "[D"
 179: lload         8
 181: invokestatic  #46                 // Method clojure/lang/RT.intCast:(J)I
 184: dload         12
 186: dload         18
 188: dmul
 189: dload         16
 191: dload         14
 193: dmul
 194: dadd
 195: invokestatic  #55                 // Method clojure/lang/RT.aset:([DID)D
 198: pop2
 199: lload         8
 201: lconst_1
 202: ladd
 203: lload         10
 205: lstore        10
 207: lstore        8
 209: goto          91

Java 代码:

class ComplexArray {

    static final int SIZE = 1 << 16;

    double re[];

    double im[];

    ComplexArray(double re[], double im[]) {
        this.re = re;
        this.im = im;
    }

    static ComplexArray getZero() {
        return new ComplexArray(new double[SIZE], new double[SIZE]);
    }

    ComplexArray multiply(ComplexArray second) {
        double resultRe[] = new double[SIZE];
        double resultIm[] = new double[SIZE];
        for (int i = 0; i < SIZE; i++) {
            double aRe = this.re[i];
            double aIm = this.im[i];
            double bRe = second.re[i];
            double bIm = second.im[i];
            resultRe[i] = aRe * bRe - aIm * bIm;
            resultIm[i] = aRe * bIm + bRe * aIm;
        }
        return new ComplexArray(resultRe, resultIm);
    }

    public static void main(String args[]) {
        ComplexArray a = getZero();
        for (int i = 0; i < 30000; i++) {
            a = a.multiply(a);
        }
        System.out.println(a.re[0]);
    }
}

Java 代码中相同循环的汇编:

  13: iload         4
  15: ldc           #5                  // int 65536
  17: if_icmpge     92
  20: aload_0
  21: getfield      #2                  // Field re:[D
  24: iload         4
  26: daload
  27: dstore        5
  29: aload_0
  30: getfield      #3                  // Field im:[D
  33: iload         4
  35: daload
  36: dstore        7
  38: aload_1
  39: getfield      #2                  // Field re:[D
  42: iload         4
  44: daload
  45: dstore        9
  47: aload_1
  48: getfield      #3                  // Field im:[D
  51: iload         4
  53: daload
  54: dstore        11
  56: aload_2
  57: iload         4
  59: dload         5
  61: dload         9
  63: dmul
  64: dload         7
  66: dload         11
  68: dmul
  69: dsub
  70: dastore
  71: aload_3
  72: iload         4
  74: dload         5
  76: dload         11
  78: dmul
  79: dload         9
  81: dload         7
  83: dmul
  84: dadd
  85: dastore
  86: iinc          4, 1
  89: goto          13

【问题讨论】:

  • 为什么不直接使用 Clojure 的 Java 实现?
  • @OlegTheCat 有可能,如果有一种理想的方式来编写这样的 Clojure 代码,Clojure 编译器可以创建最佳代码,我只是徘徊。
  • @OlegTheCat 有趣的引用是来自clojure.org/reference/java_interop 的“结果代码的速度完全相同”。我想知道该示例是规则(并且 Clojure 中的数组处理可能很有效)还是异常。
  • @SamEstep 获取程序集的具体步骤: 1.lein new app tmp 2.编辑tmp/src/tmp/code.clj - 将def sizedefn get-zero-complex-arraydefn multiply-complex-arrays放在@之间987654344@ 和(defn -main。 3. lein uberjar 4. 从 target/uberjar/tmp-0.1.0-SNAPSHOT.jar 中提取 tmp/core$multiply_complex_arrays.class 5. javap -p -c core$multiply_complex_arrays &gt;src
  • (set! *unchecked-math* true) 会将intCast 调用转换为l2i 指令

标签: arrays performance clojure clojure-java-interop


【解决方案1】:

您如何对这段代码进行基准测试?我建议在比较时间之前使用诸如 criterium 之类的东西,或者至少执行多次执行。当它足够温暖时,JIT 应该优化 checkcast 之类的东西。我还建议使用最新的 JVM、-server 和 -XX:+AggressiveOpts。

一般来说,我发现最好不要尝试强制 Clojure 在循环中使用整数 - 而是将长整数作为循环计数器,使用 (set! *unchecked-math* true),并让 Clojure 在索引数组时将长整数向下转换为整数。虽然这似乎是额外的工作,但我对现代硬件/JVM/JIT 的印象是差异远小于您的预期(因为您主要使用 64 位整数)。此外,看起来您将 size 作为循环变量携带,但它永远不会改变 - 也许您这样做是为了避免与 i 的类型不匹配,但我只会在循环之前让 size(作为 long)并做长增量和而是在 i 上进行比较。

有时你可以通过让事情在循环之前减少检查。虽然很容易观察代码并说出何时不需要它们,但编译器并没有真正对此进行任何分析,而是将其留给 JIT 来优化事情(它通常很擅长,或者不擅长) t 实际上在 99% 的代码中都很重要)。

(set! *unchecked-math* :warn-on-boxed)

(def ^long ^:const size 65536)

(defn get-zero-complex-array []
  {:re (double-array size)
   :im (double-array size)})

(defn multiply-complex-arrays [a b]
  (let [a-re-array (doubles (get a :re))
        a-im-array (doubles (get a :im))
        b-re-array (doubles (get b :re))
        b-im-array (doubles (get b :im))
        res-re-array (double-array size)
        res-im-array (double-array size)
        s (long size)]
    (loop [i 0]
      (if (< i s)
        (let [a-re (aget a-re-array i)
              a-im (aget a-im-array i)
              b-re (aget b-re-array i)
              b-im (aget b-im-array i)]
          (aset res-re-array i (- (* a-re b-re) (* a-im b-im)))
          (aset res-im-array i (+ (* a-re b-im) (* b-re a-im)))
          (recur (inc i)))
        {:re res-re-array :im res-im-array}))))

(defn compute []
  (let [res (loop [i 0 a (get-zero-complex-array)]
              (if (< i 30000)
                (recur (inc i) (multiply-complex-arrays a a))
                a))]
    (aget (get res :re) 0)))

【讨论】:

  • 在使用 *unchecked-math* 之后,时间从 1.34 java 变为 1.22 java,在将 size 移出循环并重复到 let 之后(比您在双数组中使用它的建议高 2 行) ,时间到了 1.17 java,^long ^:consti 类型为 long,时间到了 1.15 java。亚历克斯,感谢您提出宝贵的建议。
  • 我在 Windows 下使用time 程序的输出来测量挂墙时间。我跑了 5 次,得到了中位数。为了获得多个 Java 时间,我减去执行 30000 次循环的时间和在 Java 和 Clojure 中执行 1 次循环的时间,然后将 Clojure 差异除以 Java 差异。
  • 这个问题和竞技编程有关,所以代码没有时间运行超过10秒,所以预热几分钟后就不测了。
  • (set! *unchecked-math* :warn-on-boxed) 是全局设置吗?怎么可能只应用到一个函数甚至一个循环?
  • *unchecked-math* 是一个动态变量,因此它既有根绑定(false),也可以在每个线程的基础上设置(这里)。它不能只应用于一个函数或循环,除非您创建一个宏来设置和重置函数周围的值。
猜你喜欢
  • 1970-01-01
  • 2021-06-23
  • 2019-01-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多