4

介绍

这是我之前提出的问题的后续问题: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 秒

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:

我用新的轮函数更改了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++ 编译我的所有代码,并最终获得我的算法所需的速度。:)

4

9 回答 9

21

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

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

cout << temp << "\n";

您对基准测试的了解不足以比较这类东西(我的意思是比较单个数学函数)。我建议购买一本关于测试和基准测试的书。

于 2011-06-22T19:30:50.290 回答
7

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

确保您没有在 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;

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

5157ms for 100000000 log10s

5187ms for 100000000 log10s(双循环计数器)

100000000 log10s 为 5312ms(双循环计数器,四舍五入为 fn)

于 2011-06-22T19:26:02.193 回答
4

就像@Mat 评论的那样,您的 C++round与 Javas 不同Math.roundOracle 的 Java 文档说这Math.round(long)Math.floor(a + 0.5d).

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

于 2011-06-22T19:40:28.253 回答
2

总结一下其他人在这里所说的内容:C++ iostream 功能在 Java 中的实现方式与此不同。在 C++ 中,输出到 IOStreams 会在输出每个字符之前创建一个名为 sentry 的内部类型。例如 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 选项)已被删除。通过 /MT 和 /MTd 选项使用 LIBCPMT.LIB 和 LIBCPMTD.LIB。

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

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

产生两个锁:一个用于“test”,第二个用于“\n”。如果您调试到 operator << 实现,这将变得很明显:

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

用 '\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 的建议试试我的建议。现在时间安排如何?

于 2011-06-22T19:50:50.623 回答
2

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

于 2011-06-22T19:24:59.783 回答
2

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

浮点语义的 fp:fast 模式

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

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

  • cl -fp:fastsource.cpp
  • cl /fp:fast源码.cpp

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

甲骨文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("%F\n", 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);
            }
        }
    }
}
于 2011-06-22T21:11:30.713 回答
1

可能想看看这里

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

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

于 2011-06-22T19:42:30.097 回答
1

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

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

C++ 程序比 Java 或 C# 程序慢的感觉来自于在调试模式(默认)下构建它们。Cay Horstman,一位备受赞誉的 C++ 和 Java 书籍作者,在 Addison Wesley(2002 年)的“Core Java 2”中落入了这个陷阱。

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

于 2011-06-22T20:27:51.543 回答
0

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

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

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

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

于 2011-06-22T22:20:30.740 回答