【问题标题】:Why is this code so faster in java than in C++ and C#为什么这段代码在 java 中比在 C++ 和 C# 中快
【发布时间】:2016-01-30 10:12:03
【问题描述】:

我正在做一个简单的作业,我必须用 C 语言开发一个软件,以找到其中 100 个点之间最近的两个点。

完成后,我很想知道运行它需要多少时间才能获得更多积分并启用完整的 VC++ 优化。我尝试了 10000,大约需要 8~9 秒。然后我很想知道 C# 和 Java 需要多少时间来做同样的事情。不出所料,C# 用的时间稍长一些,9~10 秒;然而,Java 只用了大约 400 毫秒!为什么会这样?!

这是我的 C、C# 和 Java 代码:

C:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <Windows.h>

long perfFrequency = 0;

typedef struct
{
    double X;
    double Y;
} Point;

double distance(Point p1, Point p2)
{
    return sqrt(pow(p1.X - p2.X, 2) + pow(p1.Y - p2.Y, 2));
}

double smallerDistance(Point *points, int size, Point *smallerA, Point  *smallerB)
{
    int i, j;
    double smaller = distance(points[0], points[1]);

    for (i = 0; i < size; i++)
    {
        for (j = i + 1; j < size; j++)
        {
            double dist = distance(points[i], points[j]);
            if (dist < smaller)
            {
                smaller= dist;
                *smallerA = points[i];
                *smallerB = points[j];
            }
        }
    }
    return smaller;
}

void main()
{
    // read size and points from file.
    int size;
    Point *points= (Point *)malloc(size * sizeof(Point));

    // just to make sure everything is ready before the benchmark begins    
    system("pause");

    Point smallerA, smallerB;
    if (!QueryPerformanceFrequency((LARGE_INTEGER *)&perfFrequency))
        printf("Couldn't query performance frequency.");

    long long start, end;   
    double smaller;
    QueryPerformanceCounter((LARGE_INTEGER *)&start);

    smaller= smallerDistance(points, size, &smallerA, &smallerB);

    QueryPerformanceCounter((LARGE_INTEGER *)&end);

    printf("The smaller distance is: %lf. The coordinates of the most close points are: (%lf, %lf) and (%lf, %lf). Time taken: %lfms\n",
        smaller, smallerA.X, smallerA.Y, smallerB.X, smallerB.Y, (end - start) * 1000.0 / perfFrequency);

}

C#:

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace StructuredTest
{
    struct Point
    {
        public double X;
        public double Y;
    }

    class Program
    {
        static double Distance(Point p1, Point p2)
        {
            return Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2));
        }

        static double SmallerDistance(Point[] points, int size, out Point smallerA, out Point smallerB)
        {
            int i, j;
            double smaller = Distance(points[0], points[1]);
            smallerA = default(Point);
            smallerB = default(Point);

            for (i = 0; i < size; i++)
            {
                for (j = i + 1; j < size; j++)
                {
                    double dist = Distance(points[i], points[j]);
                    if (dist < smaller)
                    {
                        smaller = dist;
                        smallerA = points[i];
                        smallerB = points[j];
                    }
                }
            }

            return smaller;
        }

        static void Main(string[] args)
        {
            // read size and points from file 
            int size = int.Parse(file[0]);
            Point[] points= new Point[size];                   

            // make sure everything is ready
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey(true);

            Point smallerA, smallerB;
            double smaller;

            Stopwatch sw = new Stopwatch();
            sw.Restart();

            smaller = SmallerDistance(points, size, out smallerA, out smallerB);

            sw.Stop();

            Console.WriteLine($"The smaller distance is: {smaller}. The coordinates of the most close points are: ({smallerA.X}, {smallerA.Y}) and " +
                $"({smallerB.X}, {smallerB.Y}). Time taken: {sw.ElapsedMilliseconds}ms.");

        }
    }
}

Java:

package structuredtest;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

class Point {

    public Point(double X, double Y) {
        this.X = X;
        this.Y = Y;
    }

    double X;
    double Y;
}

class Result {

    double distance;
    Point p1;
    Point p2;
}

public class StructuredTest {

    static double distance(Point p1, Point p2) {
        return Math.sqrt(Math.pow(p1.X - p2.X, 2) + Math.pow(p1.Y - p2.Y, 2));
    }

    static Result smallerDistance(Point[] points, int size) {
        int i, j;
        double smaller = distance(points[0], points[1]);
        Result r = new Result();

        for (i = 0; i < size; i++) {
            for (j = i + 1; j < size; j++) {
                double dist = distance(points[i], points[j]);
                if (dist < smaller) {
                    smaller = dist;
                    r.p1 = points[i];
                    r.p2 = points[j];
                }
            }
        }

        r.distance = smaller;
        return r;
    }

    public static void main(String[] args) throws IOException {
        // read size and points from file
        int size = Integer.parseInt(file[0]);
        Point[] points = new Point[size];

        // make sure everything is ready    
        System.out.println("Press any key to continue...");
        System.in.read();

        double start = System.nanoTime(), end;

        Result r = smallerDistance(points, size);

        end = System.nanoTime();

        System.out.println("The smaller distance is: " + r.distance + ". The most close points are: ("
                + r.p1.X + "," + r.p1.Y + ") and " + r.p2.X + "," + r.p2.Y + "). Time taken: " + (end - start) / 1000000 + "ms.");

    }

}

如果 java 以小幅度击败 C 和 C#,我不会感到惊讶,但要快 20 倍?!

文件格式如下:

3 // number of points in the file. Note that there no comments in the actual file
(3.7098722472288, 4.49056397953787) // point (X,Y)
(8.90232811621332, 9.67982769279173)
(5.68254334818822, 1.71918922506136)
(6.22585901842366, 9.51660500242835)

有趣的是:起初,我之前提到的用于基准测试的具有 10000 个点的文件实际上只是另一个具有 100 个随机点的文件的 100 次复制粘贴。像这样:

(Point 1)
(Point 2)
(Point 3)
(Point 1)
(Point 2)
(Point 3)

我认为没有必要生成 10000 个随机点,因为无论如何代码都必须遍历所有数字,所以差别不大(只有更多的分配)。但后来我决定生成 10000 个随机点,看看它们会如何反应:C 和 C# 仍然在大约相同的时间内运行(增加约 50 毫秒);另一方面,Java 增加了约 500 毫秒。

另外,我认为值得注意的是,java 在 NetBeans 中运行时大约需要 11 秒(即使在“运行”模式下,而不是“调试”模式下)。

我也尝试编译为 C++ 而不是 C,但没有任何区别。

我将 VS 2015 用于 C 和 C#。

这些是每种语言的设置:

C:

x64
Optimization: Maximize Speed (/O2)
Intrinsic Functions: Yes (/Oi)
Favor Size or Speed: Favor fast code (/Ot)
Enable Fiber-Safe Optimizations: Yes (/GT)
Security Check: Disable Security Check (/GS-)
Floating point model: Fast (/fp:fast)
Everything else: default

C#:

x64
Release Mode
Optimize Code: Enabled
Check for arithmetic overflow: Disabled
.NET 4.5.2 

Java:

JRE/JDK 1.8
Default settings (if there are any)

编辑:

好的,我按照建议重新做了测试:

首先,我在 C 和 C# 中都使用了 Result 类/结构。我在 java 中使用它而不是在 C/C# 中使用它的原因是因为 java 不能通过引用传递。 其次,我现在在 main() 函数中重复测试。 感谢@Tony D 发现了这个错误! :)

我不会发布代码,因为更改很小:只需在其他测试中完全实现 java 版本,这就是我所做的。

这次我只测试了 7000 点(不是 10000 点)并且只进行了 30 次迭代,因为测试需要很长时间,而且这里已经很晚了。

结果变化不大:C# 平均耗时 5228 毫秒,C 4424 毫秒,Java 223 毫秒。 Java 仍然以快 20 倍或更多倍的速度获胜。

然后我尝试删除对 Math.Pow 的调用(只需更改为 ((p1.X - p2.X) * (p1.X - p2.X)) + ((p1.Y - p2.Y) * (p1.Y - p2.Y))),然后一切都改变了。新结果:

Java:平均 220 毫秒

C#:平均 195 毫秒

C:平均 195 毫秒

如果我之前只检查过:p

正如我评论的那样,我考虑过这样做,但后来决定最好测试每个编译器内联函数的能力并优化这种简单的调用。然而,当我得到那些奇怪的结果时,我应该回去做这个,但我太紧张了,我忘了做。

不管怎样,老实说,我很惊讶 Java 编译器能够完全优化那行代码,而 C# 和 C++ 却没有。尽管我知道 C# 上的极端情况检查和内部调用,但我发现 Java 编译器能够注意到在该代码中根本不需要极端情况检查,这真的很有趣。

【问题讨论】:

  • 也许 Java 正在优化对乘法的 Math.pow(p1.X - p2.X, 2) 调用:您可以在 C 版本中将其显式更改为乘法,看看这是否解释了性能差异。 QueryPerformanceCounter 也是出了名的不可靠,这取决于您的确切操作系统版本和硬件,但如果您的机器处于空闲状态并且它不会跨内核翻转程序,则您不应该受到错误的影响。
  • 为什么java版本有Result类,而C#和C++版本没有?你确定你是在比较苹果和苹果吗?这 3 个程序的输出是否相同?
  • 这是在强制性的 40 秒热身之后吗?
  • 我已将程序复制到 VC++2012 中 - 用随机数初始化 1000 个点,这台 PC 上大约需要 6.8 毫秒。对于 10k 点,约 738 毫秒。发布模式构建不考虑其他优化设置。 (你有一个错误:如果不是其他点比点 [0] 和点 [1] 更靠近,你永远不会设置 *smallerA*smallerB)。避免 pow 支持手动乘法:~700ms。
  • 在摆弄东西时,最大的不同是将 sqrt(pow(p1.X - p2.X, 2) + pow(p1.Y - p2.Y, 2)) 更改为 sqrt((p1.X - p2.X)*(p1.X - p2.X) + (p1.Y - p2.Y)*(p1.Y - p2.Y)); 花费了 1/10 的时间。 (10000 个随机点,4500mS 到 322mS)这是 2015 社区版

标签: java c# c++ c performance


【解决方案1】:

正如 here 和检查 here 所解释的,在纯 C 中,没有整数幂的重载,就像这样:

double pow(double base, int exponent );

表示当你在C中调用pow时,其处理方式类似于:

double pow(double base, double exponent) {
    return exp(log(base) * exponent);
}

还应检查负基数和整数幂的情况,以特殊方式处理。在此处添加if (exponent == 1.0)if (exponent == 2.0) 之类的条件并不是一个好主意,因为它会减慢真正使用pow 的数学代码的速度。结果,平方变慢了in twelve times or something like that

原则上,将pow(x, 2) 优化为x * x 的唯一合理方法是让编译器识别此类反模式并为其生成特殊代码。它只是发生了,您的 C 和 C# 编译器与您的设置无法做到这一点,而 Java 编译器可以做到这一点。请注意,它与内联功能无关:仅内联 exp(log(a) * b) 不会使此代码更快。

作为结论,我想指出,任何擅长编写数学代码的程序员都不会在他的代码中写pow(x, 2)

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-22
  • 2015-03-26
  • 2012-12-03
  • 2010-09-17
  • 1970-01-01
  • 2015-01-16
相关资源
最近更新 更多