0

我有两个函数,每个函数都计算两个不同向量的余弦相似度。一种是用 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

爪哇:

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 CosineSimilarity

耗时 44 秒

编辑:

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

4

4 回答 4

3

Math.pow(a, b) 执行 math.exp( math.log (a)*b) 将是一种非常昂贵的求平方数的方法。

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

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

于 2015-01-01T22:57:29.897 回答
3

我在紧密的图形循环中看到了 2 的因数。从来没有 15。

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

于 2015-01-01T23:30:57.733 回答
1

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

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

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

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

于 2015-01-01T23:15:19.280 回答
1

使用静态数组的速度可能不是 15 倍,而是 10 倍。乘法更容易进行平方。使用局部变量 forvectorA[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));
}
于 2015-01-01T23:22:59.990 回答