Java 中的余弦相似性性能比等效 C 慢 15 倍?

Cosine-similarity performance in Java 15 times slower than equivalent C?

我有两个函数,每个函数计算两个不同向量的余弦相似度。一个是用 Java 写的,一个是用 C 写的。

在这两种情况下,我都声明两个 200 个元素的数组内联,然后计算它们的余弦相似度 100 万次。我没有计算 jvm 启动的时间。 Java 实现比 C 实现慢近 15 倍。

我的问题是:

1.) 假设对于简单数学的紧密循环 c 仍然比 java 快一个数量级是否合理?

2.) java 代码中是否有错误,或者一些可以显着加快速度的合理优化?

谢谢。

C:

#include <math.h>

int main()
{
  int j;
  for (j = 0; j < 1000000; j++) {
    calc();
  }

  return 0;

}

int calc ()
{

  double a [200] = {0.269852, -0.720015, 0.942508, ...};
  double b [200] = {-1.566838, 0.813305, 1.780039, ...};

  double p = 0.0;
  double na = 0.0;
  double nb = 0.0;
  double ret = 0.0;

  int i;
  for (i = 0; i < 200; i++) {
    p += a[i] * b[i];
    na += a[i] * a[i];
    nb += b[i] * b[i];
  }

  return p / (sqrt(na) * sqrt(nb));

}

$时间./余弦相似度

0m2.952s

Java:

public class CosineSimilarity {

            public static void main(String[] args) {

                long startTime = System.nanoTime();

                for (int i = 0; i < 1000000; i++) {
                    calc();
                }

                long endTime = System.nanoTime();
                long duration = (endTime - startTime);

                System.out.format("took %d%n seconds", duration / 1000000000);

            }

            public static double calc() {

                double[] vectorA = new double[] {0.269852, -0.720015, 0.942508, ...};
                double[] vectorB = new double[] {-1.566838, 0.813305, 1.780039, ...};

                double dotProduct = 0.0;
                double normA = 0.0;
                double normB = 0.0;
                for (int i = 0; i < vectorA.length; i++) {
                    dotProduct += vectorA[i] * vectorB[i];
                    normA += Math.pow(vectorA[i], 2);
                    normB += Math.pow(vectorB[i], 2);
                }
                return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
            }
    }

$java-cp。 -server -Xms2G -Xmx2G 余弦相似度

用了 44 秒

编辑:

Math.pow 确实是罪魁祸首。删除它使性能与 C 的性能相当。

Math.pow(a, b) 做 math.exp( math.log (a)*b) 这将是一个非常昂贵的平方数的方法。

我建议您编写 Java 代码类似于您编写 C 代码的方式以获得更接近的结果。

注意:JVM 可能需要几秒钟来预热代码。我会 运行 测试更长时间。

除了关于 Math.Pow(x,2) 不能直接与 x*x 进行比较的评论外,请参阅有关基准测试的其他答案 java。 TL,DR: 正确地做事并不简单或容易。

由于 Java 环境包括执行时编译(JIT 编译器),并且可能包括执行时动态优化("Hotspot" 和类似技术),获得有效 Java性能数字很复杂。您需要指定您是否对早期性能或稳态性能感兴趣,如果是后者,您需要在开始测量之前让 JRE 预热——即使如此,对于明显相似的输入,结果可能会有很大不同集。

更糟糕的是,JIT 编译顺序在某些 JRE 中是不确定的;连续执行可能会选择以不同的顺序优化代码。对于特别大的 Java 应用程序,您可能会发现 JRE 对以完全 JIT 形式保留的代码量有限制,因此编译顺序的变化会对性能产生惊人的影响。即使在完全预热并排除 GC 和其他异步操作的影响后,我发现某些 JRE 的某些版本可能显示 运行-to-运行 的性能变化高达 20%完全相同的代码和输入。

Java can 表现出奇的好,因为 JIT 编译器使它成为一种(后期)编译语言。但是微基准测试通常会产生误导,甚至宏基准测试也可能必须对多次加载(而不仅仅是多次执行)进行平均才能获得可靠有意义的数字。

使用静态数组可能会加速15 倍,但可能是10 倍。乘法更容易求平方。为 vectorA[i] 使用局部变量更像是一种风格问题,甚至可能会使编译器优化更加困难。

static final double[] vectorA = {0.269852, -0.720015, 0.942508, ... };
static final double[] vectorB = {-1.566838, 0.813305, 1.780039, ... };

public static double calc() {
    double dotProduct = 0.0;
    double normA = 0.0;
    double normB = 0.0;
    for (int i = 0; i < vectorA.length; i++) {
        double a = vectorA[i];
        double b = vectorB[i];
        dotProduct += a * b;
        normA += a * a;
        normB += b * b;
    }
    return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}

我在紧密的图形循环中看到了 2 的因数。永远不会 15.

我会非常怀疑你的测试。除了已经提出的其他要点之外,请考虑许多 C 编译器(包括例如 gcc)能够推断出您的计算结果从未被使用过,因此,任意块直到并包括整个可以优化基准。您需要查看生成的代码以确定是否发生这种情况。