为什么Java似乎比C ++执行速度更快 - 第2部分

时间:2011-06-22 19:21:28

标签: java c++ visual-studio-2010 netbeans7.0

简介

这是我之前提出的问题的后续问题:Java seems to be executing bare-bones algorithms faster than C++. Why?。通过这篇文章,我学到了一些重要的东西:

  1. 我没有使用Ctrl + F5在Visual Studios C ++ Express上编译和运行c ++代码,这导致了debuging,从而减慢了代码的执行速度。
  2. 向量与处理数据数组的指针一样好(如果不是更好)。
  3. 我的C ++很糟糕。 ^ _ ^
  4. 更好的执行时间测试是迭代,而不是递归。
  5. 我尝试编写一个更简单的程序,它不使用指针(或Java等效的数组),并且在执行时非常简单。即便如此,Java执行速度也比C ++执行速度快。我做错了什么?

    代码:

    爪哇:

     public class PerformanceTest2
     {
          public static void main(String args[])
          {
               //Number of iterations
               double iterations = 1E8;
               double temp;
    
               //Create the variables for timing
               double start;
               double end;
               double duration; //end - start
    
               //Run performance test
               System.out.println("Start");
               start = System.nanoTime();
               for(double i = 0;i < iterations;i += 1)
               {
                    //Overhead and display
                    temp = Math.log10(i);
                    if(Math.round(temp) == temp)
                    {
                         System.out.println(temp);
                    }
               }
               end = System.nanoTime();
               System.out.println("End");
    
               //Output performance test results
               duration = (end - start) / 1E9;
               System.out.println("Duration: " + duration);
          }
     }
    

    C ++:

    #include <iostream>
    #include <cmath>
    #include <windows.h>
    using namespace std;
    
    double round(double value)
    {
    return floor(0.5 + value);
    }
    void main()
    {
    //Number of iterations
    double iterations = 1E8;
    double temp;
    
    //Create the variables for timing
    LARGE_INTEGER start; //Starting time
    LARGE_INTEGER end; //Ending time
    LARGE_INTEGER freq; //Rate of time update
    double duration; //end - start
    QueryPerformanceFrequency(&freq); //Determinine the frequency of the performance counter (high precision system timer)
    
    //Run performance test
    cout << "Start" << endl;
    QueryPerformanceCounter(&start);
    for(double i = 0;i < iterations;i += 1)
    {
        //Overhead and display
        temp = log10(i);
        if(round(temp) == temp)
        {
            cout << temp << endl;
        }
    }
    QueryPerformanceCounter(&end);
    cout << "End" << endl;
    
    //Output performance test results
    duration = (double)(end.QuadPart - start.QuadPart) / (double)(freq.QuadPart);
    cout << "Duration: " << duration << endl;
    
    //Dramatic pause
    system("pause");
    }
    

    观察:

    对于1E8次迭代:

    C ++执行= 6.45秒

    Java Execution = 4.64 s

    更新

    根据Visual Studios,我的C ++命令行参数是:

    /Zi /nologo /W3 /WX- /O2 /Ob2 /Oi /Ot /Oy /GL /D "_MBCS" /Gm- /EHsc /GS /Gy /fp:precise /Zc:wchar_t /Zc:forScope /Fp"Release\C++.pch" /Fa"Release\" /Fo"Release\" /Fd"Release\vc100.pdb" /Gd /analyze- /errorReport:queue
    

    更新2:

    我使用新的round函数更改了c ++代码,并更新了执行时间。

    更新3:

    我找到了问题的答案,感谢Steve Townsend和Loduwijk。在将我的代码编译成汇编并对其进行评估后,我发现C ++程序集创建的内存移动方式比Java程序集更多。这是因为我的JDK使用的是x64编译器,而我的Visual Studio Express C ++无法使用x64架构,因此本质上速度较慢。所以,我安装了Windows SDK 7.1,并使用这些编译器编译我的代码(在发行版中,使用ctrl + F5)。目前的时间比例是:

    C ++:~2.2 s    Java:~4.6秒

    现在我可以用C ++编译所有代码,最后获得我的算法所需的速度。 :)

9 个答案:

答案 0 :(得分:21)

这是一个安全的假设,即每当你看到Java超越C ++时,尤其是如此巨大的差距,你就会做错事。由于这是专门针对这种微观微观优化的第二个问题,我觉得我应该建议找一个不那么徒劳的爱好。

这回答了你的问题:你正在使用C ++(实际上,你的操作系统)是错误的。至于隐含的问题(如何?),很容易:endl刷新流,Java继续缓冲它。将您的cout行替换为:

cout << temp << "\n";

你不太了解基准测试来比较这种东西(我的意思是比较单个数学函数)。我建议买一本关于测试和基准测试的书。

答案 1 :(得分:7)

你肯定不想为输出计时。删除每个循环中的输出语句并重新运行,以便更好地比较您实际感兴趣的内容。否则,您还要对输出函数和视频驱动程序进行基准测试。产生的速度实际上取决于您在运行时控制台窗口是否在测试时被遮挡或最小化。

确保您没有在C ++中运行Debug构建。这将比发布慢很多,与您启动流程的方式无关。

编辑:我在本地重现了这个测试场景,但无法获得相同的结果。修改代码(如下所示)以删除输出,Java需要5.40754388秒。

public static void main(String args[]) { // Number of iterations 
    double iterations = 1E8;
    double temp; // Create the variables for timing
    double start;
    int matches = 0;
    double end;
    double duration;
    // end - start //Run performance test
    System.out.println("Start");
    start = System.nanoTime();
    for (double i = 0; i < iterations; i += 1) {
        // Overhead and display
        temp = Math.log10(i);
        if (Math.round(temp) == temp) {
            ++matches;
        }
    }
    end = System.nanoTime();
    System.out.println("End");
    // Output performance test results
    duration = (end - start) / 1E9;
    System.out.println("Duration: " + duration);
}

下面的C ++代码需要5062 ms。这是在Windows上的JDK 6u21和VC ++ 10 Express。

unsigned int count(1E8);
DWORD end;
DWORD start(::GetTickCount());
double next = 0.0;

int matches(0);
for (int i = 0; i < count; ++i)
{
    double temp = log10(double(i));
    if (temp == floor(temp + 0.5))
    {
        ++count;
    }
}

end = ::GetTickCount();
std::cout << end - start << "ms for " << 100000000 << " log10s" << std::endl;

编辑2: 如果我更准确地从Java恢复你的逻辑,我会得到几乎相同的C ++和Java时间,这是我所期望的,因为它依赖于log10实现。

  

5157ms,100000000 log10s

     对于100000000 log10s(双循环计数器)

5187ms

     对于100000000 log10s(双循环计数器,圆为fn)

5312ms

答案 2 :(得分:4)

与@Mat评论一样,您的C ++ round与Javas Math.round不同。 Oracle's Java documentation表示Math.round(long)Math.floor(a + 0.5d)相同。

请注意,在C ++中(而且可能在Java中),不能将long转换为long会更快。

答案 3 :(得分:2)

这是因为印刷了价值观。与实际循环无关。

答案 4 :(得分:2)

总结一下其他人在这里所说的内容:C ++ iostream功能在Java中的实现与此不同。在C ++中,输出到IOStreams会在输出每个字符之前创建一个名为sentry的内部类型。例如。 ostream :: sentry使用RAII惯用法来确保流处于一致状态。在多线程环境(在许多情况下是默认环境)中,在打印每个字符以避免竞争条件之后,哨兵还用于锁定互斥锁对象并将其解锁。互斥锁定/解锁操作非常昂贵,这就是您面临这种减速的原因。

Java走向另一个方向,只为整个输出字符串锁定/解锁互斥锁一次。这就是为什么如果你从多个线程输出到cout你会看到真正混乱的输出,但所有的字符都会在那里。

如果您直接使用流缓冲区并且仅偶尔刷新输出,则可以使C ++ IOStream执行。要测试此行为,只需关闭测试的线程支持,并且C ++可执行文件应该运行得更快。

我玩了一下流和代码。以下是我的结论:首先,没有单独的线程库从VC ++ 2008开始可用。请在下面的链接中,MS声明不再支持单线程运行时库: http://msdn.microsoft.com/en-us/library/abx4dbyh.aspx

  

注意已删除LIBCP.LIB和LIBCPD.LIB(通过旧/ ML和/ MLd选项)。请通过/ MT和/ MTd选项使用LIBCPMT.LIB和LIBCPMTD.LIB。

MS IOStreams实现实际上锁定了每个输出(不是每个字符)。因此写作:

cout << "test" << '\n';

生成两个锁:一个用于“test”,第二个用于“\ n”。如果您调试运算符&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;实现:

_Myt& __CLR_OR_THIS_CALL operator<<(double _Val)
    {// insert a double
    ios_base::iostate _State = ios_base::goodbit;
    const sentry _Ok(*this);
    ...
    }

这里操作符调用构造了sentry实例。哪个派生自basic_ostream :: _ Sentry_base。 _Sentry_base ctor锁定缓冲区:

template<class _Elem,   class _Traits>
class basic_ostream
  {
  class _Sentry_base
  {
    ///...

  __CLR_OR_THIS_CALL _Sentry_base(_Myt& _Ostr)
        : _Myostr(_Ostr)
        {   // lock the stream buffer, if there
        if (_Myostr.rdbuf() != 0)
          _Myostr.rdbuf()->_Lock();
        }

    ///...
  };
};

这导致致电:

template<class _Elem, class _Traits>
void basic_streambuf::_Lock()
    {   // set the thread lock
    _Mylock._Lock();
    }

结果:

void __thiscall _Mutex::_Lock()
    {   // lock mutex
    _Mtxlock((_Rmtx*)_Mtx);
    }

结果:

void  __CLRCALL_PURE_OR_CDECL _Mtxlock(_Rmtx *_Mtx)
    {   /* lock mutex */
  // some additional stuff which is not called...
    EnterCriticalSection(_Mtx);
    }

使用std :: endl操纵器执行代码在我的机器上给出以下时间:

Multithreaded DLL/Release build:

Start
-1.#INF
0
1
2
3
4
5
6
7
End
Duration: 4.43151
Press any key to continue . . .

使用'\ n'而不是std :: endl:

Multithreaded DLL/Release with '\n' instead of endl

Start
-1.#INF
0
1
2
3
4
5
6
7
End
Duration: 4.13076
Press any key to continue . . .

更换cout&lt;&lt;温度&lt;&lt; '\ n';使用直接流缓冲区序列化来避免锁定:

inline bool output_double(double const& val)
{
  typedef num_put<char> facet;
  facet const& nput_facet = use_facet<facet>(cout.getloc());

  if(!nput_facet.put(facet::iter_type(cout.rdbuf()), cout, cout.fill(), val).failed())
    return cout.rdbuf()->sputc('\n')!='\n';
  return false;
}

再次改善时间:

Multithreaded DLL/Release without locks by directly writing to streambuf

Start
-1.#INF
0
1
2
3
4
5
6
7
End
Duration: 4.00943
Press any key to continue . . .

最后将迭代变量的类型从double更改为size_t,并且每次都使新的double值改进运行时:

size_t iterations = 100000000; //=1E8
...
//Run performance test
size_t i;
cout << "Start" << endl;
QueryPerformanceCounter(&start);
for(i=0; i<iterations; ++i)
{
    //Overhead and display
    temp = log10(double(i));
    if(round(temp) == temp)
      output_double(temp);
}
QueryPerformanceCounter(&end);
cout << "End" << endl;
...

输出:

Start
-1.#INF
0
1
2
3
4
5
6
7
End
Duration: 3.69653
Press any key to continue . . .

现在根据Steve Townsend的建议尝试我的建议。现在的时间如何?

答案 5 :(得分:2)

也许您应该使用快速浮动point mode of MSVC

  

fp:浮点语义的快速模式

     

当启用fp:fast模式时,编译器会放宽fp:精确用于优化浮点运算时的规则。此模式允许编译器以浮点精度和正确性为代价进一步优化浮点代码的速度。通过启用fp:fast模式,不依赖于高精度浮点计算的程序可以显着提高速度。

     

使用命令行编译器开关启用fp:快速浮点模式,如下所示:

     
      
  • cl -fp:fast source.cpp   
  •   
  • cl /fp:fast source.cpp
  •   

在我的Linux机器(64位)上,时间大致相同:

oracle openjdk 6

sehe@natty:/tmp$ time java PerformanceTest2 

real    0m5.246s
user    0m5.250s
sys 0m0.000s

gcc 4.6

sehe@natty:/tmp$ time ./t

real    0m5.656s
user    0m5.650s
sys 0m0.000s

完全披露,我在书中画了所有优化标志,见下面的Makefile


Makefile文件
all: PerformanceTest2 t

PerformanceTest2: PerformanceTest2.java
    javac $<

t: t.cpp
    g++ -g -O2 -ffast-math -march=native $< -o $@

t.cpp
#include <stdio.h>
#include <cmath>

inline double round(double value)
{
    return floor(0.5 + value);
}
int main()
{
    //Number of iterations
    double iterations = 1E8;
    double temp;

    //Run performance test
    for(double i = 0; i < iterations; i += 1)
    {
        //Overhead and display
        temp = log10(i);
        if(round(temp) == temp)
        {
            printf("%F\n", temp);
        }
    }
    return 0;
}

PerformanceTest2.java
public class PerformanceTest2
{
    public static void main(String args[])
    {
        //Number of iterations
        double iterations = 1E8;
        double temp;

        //Run performance test
        for(double i = 0; i < iterations; i += 1)
        {
            //Overhead and display
            temp = Math.log10(i);
            if(Math.round(temp) == temp)
            {
                System.out.println(temp);
            }
        }
    }
}

答案 6 :(得分:1)

可能想看看here

可以有许多因素可以解释为什么您的Java代码比C ++代码运行得更快。其中一个因素可能只是对于这个测试用例,Java代码更快。我甚至不会考虑将其用作一种语言比另一种语言更快的一揽子陈述。

如果我要改变你做事的方式,我会用time命令将代码移植到linux和time runtime。恭喜,你刚刚删除了整个windows.h文件。

答案 7 :(得分:1)

您的C ++程序很慢,因为您不太了解您的工具(Visual Studio)。查看菜单下方的图标行。您将在项目配置文本框中找到“调试”一词。切换到“发布”。确保通过菜单Build | Clean project和Build | Build All Ctrl + Alt + F7完全重建项目。 (菜单上的名称可能略有不同,因为我的程序是德语)。这不是从F5或Ctrl + F5开始。

在“发布模式”下,您的C ++程序的速度是Java程序的两倍。

C ++程序比Java或C#程序慢的感觉来自于在调试模式下构建它们(默认)。此外,着名的C ++和Java书籍作者Cay Horstman也陷入了“Core Java 2”,Addison Wesley(2002)的陷阱。

经验教训是:了解您的工具,特别是当您尝试判断它们时。

答案 8 :(得分:0)

JVM可以进行运行时优化。对于这个简单的例子,我想唯一相关的优化是{{​​1}}的方法内联。保存了一些方法调用开销;在内联代码之后可以进一步优化。

观看此演示文稿,以充分了解JVM内联的强大功能

http://www.infoq.com/presentations/Towards-a-Universal-VM

这很好。这意味着我们可以使用方法构建逻辑,并且它们在运行时不会花费任何成本。当他们在70年代争论GOTO与程序时,他们可能没有看到这一点。