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

Why does Java seem to be executing faster than C++ - Part 2

本文关键字:C++ 部分 Java 执行 速度 为什么      更新时间:2023-10-16

简介

这是我之前提出的一个后续问题:Java似乎比C++更快地执行基本算法。为什么?。通过那篇帖子,我学到了一些重要的事情:

  1. 我没有使用 Ctrl + F5 在 Visual Studios C++ Express 上编译和运行 c++ 代码,这导致调试减慢了代码执行速度。
  2. 在处理数据数组方面,向量与指针一样好(如果不是更好的话(。
  3. 我的C++很糟糕。 ^_^
  4. 更好的执行时间测试是迭代,而不是递归。

我尝试编写一个更简单的程序,它不使用指针(或 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 s

Java 执行 = 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"ReleaseC++.pch" /Fa"Release" /Fo"Release" /Fd"Releasevc100.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 秒 爪哇: ~4.6 秒

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

这是一个安全的假设,任何时候你看到Java的性能优于C++,尤其是如此巨大的优势,你就做错了什么。由于这是专门讨论这种微微优化的第二个问题,我觉得我应该建议找到一个不那么徒劳的爱好。

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

cout << temp << "n";

你对基准测试的理解还不够多,无法比较这类东西(我的意思是比较单个数学函数(。我建议买一本关于测试和基准测试的书。

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

确保未在C++运行调试版本。 这将比发布慢得多,与启动过程的方式无关。

编辑:我已经在本地重现了这个测试场景,无法获得相同的结果。 修改代码(如下(以删除输出后,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 毫秒。 这是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 for 100000000 log10s

5187ms for 100000000 log10s(双环路计数器(

5312ms 用于 100000000 log10s(双环路计数器,四舍五入为 fn(

就像@Mat评论的那样,你的C++ round与Java Math.round不同。Oracle的Java文档说Math.round(long)Math.floor(a + 0.5d)是一回事。

请注意,不转换为 long 在 C++ 中会更快(也可能在 Java 中也是如此(。

这是因为值的打印。与实际循环无关。

也许你应该使用 MSVC 的快速浮点模式

浮点语义的 fp:fast 模式

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

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

  • cl -fp:fast来源.cpp
  • cl /fp:fast来源.cpp

在我的 Linux 盒子(64 位(上,时间大致相等:

Oracle OpenJDK 6

sehe@natty:/tmp$ time java PerformanceTest2 
real    0m5.246s
user    0m5.250s
sys 0m0.000s

海湾合作委员会 4.6

sehe@natty:/tmp$ time ./t
real    0m5.656s
user    0m5.650s
sys 0m0.000s

完全披露,我在书中绘制了所有优化标志,请参阅下面的 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("%Fn", temp);
        }
    }
    return 0;
}
<小时 />性能测试2.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);
            }
        }
    }
}

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

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

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

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

注意 LIBCP。LIB和LIBCPD。LIB(通过旧的/ML 和/MLd 选项(已被删除。使用 LIBCPMT。LIB和LIBCPMTD。LIB 改为通过/MT 和/MTd 选项。

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

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

生成两个锁:一个用于"测试",另一个用于""。如果您调试到运算符<<实现,这一点变得很明显:

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

在这里,运算符调用构造哨兵实例。它源自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 . . .

用""代替 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 <<temp <<''; 替换为直接流缓冲区序列化以避免锁定:

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 . . .
最后,将

迭代变量的类型从双精度更改为 size_t,并使每次都使用新的双精度值,这也改善了运行时:

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 . . .

现在试试我的建议和史蒂夫汤森的建议。现在时机如何?

可能想看看这里

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

如果我对你做事的方式进行一次更改,我会使用 time 命令将代码移植到 linux 和时间运行时。恭喜,您刚刚删除了整个 windows.h 文件。

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

在"发布模式"下,C++程序的速度大约是Java程序的两倍。

认为C++程序比 Java 或 C# 程序慢的看法来自于在调试模式(默认(下构建它们。Cay Horstman,一位值得赞赏的C++和Java书籍作者,在"Core Java 2"中落入了这个陷阱,Addison Wesley(2002(。

教训是:了解你的工具,特别是当你试图判断它们时。

JVM可以进行运行时优化。对于这个简单的例子,我想唯一相关的优化是Math.round()的方法内联。节省了一些方法调用开销;并且在内联扁平化代码后可以进一步优化。

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

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

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