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

4

介绍

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

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

我尝试编写一个更简单的程序,它不使用指针(或Java等价物中的数组),并且其执行非常直接。即使如此,Java的执行速度也比C++的执行速度更快。我做错了什么?

代码:

Java:

 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执行时间 = 4.64秒

更新:

根据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秒 Java:约4.6秒

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


9
为什么Java不可能比C++更快? - Steve Kuo
8
你的C++ round函数与Math.round(double)不同。 - Mat
6
C++提供了很多工具来优化性能。但是,除非你知道如何使用它们,否则C++并不会比其他语言更快“神奇”。 - fredoverflow
5
@SpeedIsGood(哇...我猜名字已经说明了一切):你可以用任何语言编写慢代码,你应该能够在许多语言中编写快速代码(有些语言实际上很,那里无法处理),但语言的影响比你对其使用的影响要小。 - David Rodríguez - dribeas
7
因为假定高质量编写的Java和C++代码,所以它并不简单。 - Konrad Rudolph
显示剩余26条评论
9个回答

21

可以安全地假设,任何时候当你看到Java表现比C++好,特别是差距很大的时候,你肯定做错了些什么。由于这已经是第二个专注于这样的微小优化问题的提问,我觉得我应该建议找一个不那么徒劳无益的爱好。

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

cout << temp << "\n";

你对基准测试的理解不足以比较这种东西(我指的是比较单一的数学函数)。我建议购买一本关于测试和基准测试的书籍。


3
打印8行输出需要3秒钟,是否清空缓冲区都不影响。 - Joe
1
此外,这可能是徒劳无功的,但这不是一项爱好。这只是一个实验。 - SpeedIsGood
1
我认为它没有刷新...在多线程环境中,它会锁定流。请参见下面的答案以获取解释。就我所记得的,C++标准定义了任何字符都会被刷新到流中(我指的是ostream)。 - ovanes
11
这让我想起了几年前在网上读到的一句名言:“在比较不同语言时,令人惊讶的结果往往源于观察者只熟悉一种语言而对另一种语言知之甚少或完全不了解。”这句话非常正确。 - Damon
1
这个怎么被修改了?在遭受了很多人身攻击之后,给出的答案毫无意义。Java的println()也会刷新流。 - irreputable
显示剩余7条评论

7
你肯定不想计时输出。在每个循环内部删除输出语句并重新运行,以获得您实际感兴趣的更好比较。否则,您还将对输出函数和视频驱动程序进行基准测试。最终速度实际上可能取决于您运行的控制台窗口是否被遮挡或在测试时被最小化。
确保您没有在C ++中运行Debug版本。无论如何启动进程,这都比Release慢得多。
编辑:我已在本地重现了此测试方案,并且无法获得相同的结果。使用您修改后的代码(下面)删除输出,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实现。

100000000个log10的计算时间为5157毫秒

100000000个log10的计算时间为5187毫秒(双重循环计数器)

100000000个log10的计算时间为5312毫秒(双重循环计数器,round as fn)


当我删除输出命令时,时间几乎完全相同。 - SpeedIsGood
我正在VSE C++中以发布版运行它,使用ctrl + f5进行无调试构建。 - SpeedIsGood
3
@SpeedIsGood: ctrl + f5 to build without debugging Ctrl-F5 并不会构建发布模式; 你需要切换到“Release” 构建配置(不知道VCExpress把它藏在哪里) - sehe
1
@sehe - 请查看OP编辑中明确指出的编译器选项,其中包括Release。 - Steve Townsend
@Steve Townsend:我运行了你的代码,C++ 的执行时间和之前一样。 - SpeedIsGood
显示剩余10条评论

4

像 @Mat 评论的那样,你的 C++ round 和 Java 的 Math.round 不一样。Oracle's Java documentation 表示,Math.round(long)Math.floor(a + 0.5d) 是相同的。

请注意,在 C++ 中不进行 long 强制转换会更快(在 Java 中也可能是这样)。


我已经修改了代码,现在C++代码的执行速度快了约一秒钟。不过,我能让它运行得更快吗?谢谢。 - SpeedIsGood

2
简要概括其他人在这里所说的:C++ iostream 功能在 Java 中实现方式不同。在 C++ 中,输出到 IOStreams 会在输出每个字符之前创建一个名为 sentry 的内部类型。例如,ostream::sentry 使用 RAII 模式确保流处于一致状态。在多线程环境下(通常是默认情况下),sentry 还用于锁定互斥对象并在打印每个字符后解锁它,以避免竞态条件。互斥锁/解锁操作非常昂贵,这就是你面临这种减速的原因。
Java 则采用另一种方法,仅在整个输出字符串中锁定/解锁互斥锁一次。这就是为什么如果您从多个线程输出到 cout,则会看到真正混乱的输出但所有字符都将存在。
如果直接使用流缓冲区并仅偶尔刷新输出,则可以使 C++ IOStreams 具有高性能。要测试此行为,请关闭测试的线程支持,您的 C++ 可执行文件应该运行得更快。
我对 Stream 和代码进行了一些操作。这是我的结论:首先,从 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”。如果您调试到运算符<<的实现中,这一点就变得很明显了。
_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构造函数对缓冲区进行锁定:
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 << temp << '\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 的建议。现在的时间安排如何?

如何关闭线程支持?谢谢。 - SpeedIsGood
1
您可以使用/ML命令行选项,或者进入项目属性/C/C++/代码生成/运行库并选择单线程运行时库变量。在我的Visual C++ 2011 Express中,我不再看到该选项。在属性/C/C++/命令行中设置编译器效果很好。但是您必须确保所有项目链接到相同版本或运行时库。 - ovanes
我尝试去做那个,但编译器报错:cl : 命令行警告 D9002: 忽略未知选项 '/ML' - SpeedIsGood
您使用命令行选项还是VC Express对话框?我能够在没有任何警告的情况下设置此选项。这是MSDN页面的链接:http://msdn.microsoft.com/zh-cn/library/2kzt1wy3(v=vs.71).aspx - ovanes
1
似乎微软不再支持单线程运行时库。因为我发送给你的链接也适用于VC++ 7.1,而我找不到VC++ 2011相同的选项。现在已经很晚了。我明天将能够使用以前的编译器版本测试您的代码,并在此处报告我的发现。我非常确定,这取决于流锁定问题。 - ovanes
显示剩余3条评论

2

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

浮点语义的fp:fast模式

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

使用命令行编译器开启fp:fast浮点模式如下:

  • 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。
all: PerformanceTest2 t

PerformanceTest2: PerformanceTest2.java
    javac $<

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

#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;
}

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);
            }
        }
    }
}

“-O3” 不是 GCC 的最佳优化选项吗? - Xeo
我讨厌不能快速进行gcj编译的Java版本测试。我很想查看实际生成的代码,但不知何故我的SSD在安装过程中半途冻结了。糟糕的事情总会发生 :) - sehe
@self:关于GCJ - 我通过将根文件系统(live!)迁移到另一个SSD(长命lvm2!)来解决了我的问题;不幸的是:编译成本地代码后速度变慢了(7.9秒)。 - sehe
1
@sehe:/fp:fast选项显著降低了算法的运行时间。但是,/fp:fast有什么缺点?这段代码没有使用双精度,但是如果我想使用非常精确的值,/fp:fast会对代码造成问题吗?谢谢! - SpeedIsGood
@SpeedIsGood:我不知道,但(a)链接的文章应该是权威参考,因为它来自MS编译器团队(b)并且该标志经常出现在StackOverflow上(搜索strict floating point)。 - sehe
显示剩余3条评论

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

5
"endl" 会强制刷新流... - Dark Falcon
2
有大约3.3秒的差异,这似乎不太可能。除非我弄错了,否则没有很多打印出来的东西。 - Baffe Boyois
1
当我删除输出命令时,时间几乎完全相同。 - SpeedIsGood

1

你可能想要看一下这里

有很多因素可能解释为什么你的Java代码比C++代码运行更快。其中一个可能是对于这个测试用例,Java代码更快。但我不会认为这是一个适用于一种语言比另一种语言更快的普遍性陈述。

如果我要改变你的做法之一,我会将代码移植到Linux并使用 time 命令计时。恭喜你,你刚刚消除了整个windows.h文件。


谢谢链接。在Linux中,如何使用C++进行高精度计时?我会尝试你所说的。谢谢。 - SpeedIsGood
据我所知,time命令对于这些简单的测试应该足够精确。我用它来检查我在C++中编写的一些算法的复杂性。 - LainIwakura
当我使用时间命令(使用#include <ctime>)时,它似乎返回以秒为单位的值。我能否获得比这更精确的值?谢谢。 - SpeedIsGood
2
我指的是来自shell的time命令。 time *程序名称*。对我来说,它以秒为单位给出时间,包括毫秒。除此之外,我不知道如何更精确地获取时间。 - LainIwakura
@SpeedIsGood:这是我在这里使用的。 - sehe

1

你的C++程序运行缓慢是因为你不够熟悉你的工具(Visual Studio)。看一下菜单下面的图标行,你会在项目配置文本框中找到“Debug”这个词。切换到“Release”模式。通过菜单Build|Clean project和Build|Build All或Ctrl+Alt+F7来确保你完全重建了项目。(由于我的程序是德语的,你的菜单上的名称可能略有不同)。这与使用F5或Ctrl+F5启动无关。

在“Release模式”下,你的C++程序大约比Java程序快两倍。

认为C++程序比Java或C#程序慢的观念来自于在Debug模式下构建它们(默认设置)。此外,C++和Java书籍作者Cay Horstman在《Core Java 2》(Addison Wesley,2002)中也陷入了这个陷阱。

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


1
我已经将其发布模式下运行。我已经多次清理和重建它。它仍然以相同的执行速度运行。 - SpeedIsGood

0
JVM 可以进行运行时优化。对于这个简单的例子,我猜唯一相关的优化是 Math.round() 方法的内联。可以节省一些方法调用开销;在内联后,进一步优化也是可能的。
观看这个演示,完全领会 JVM 内联的强大之处。

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

这很好。这意味着我们可以使用方法来构建逻辑,而它们在运行时不会产生任何成本。当他们在70年代争论GOTO与过程时,他们可能没有预料到这一点。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接