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
)能够推断出您的计算结果从未被使用过,因此,任意块直到并包括整个可以优化基准。您需要查看生成的代码以确定是否发生这种情况。
我有两个函数,每个函数计算两个不同向量的余弦相似度。一个是用 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
)能够推断出您的计算结果从未被使用过,因此,任意块直到并包括整个可以优化基准。您需要查看生成的代码以确定是否发生这种情况。