【问题标题】:Why is an integer array search loop slower in C++ than Java?为什么 C++ 中的整数数组搜索循环比 Java 慢?
【发布时间】:2019-05-19 16:38:08
【问题描述】:

我用 C++ 和 Java 编写了相同的程序。对于 C++,我使用 VS 2019,对于 Java,我使用 Eclipse 2019-03。

这是 C++ 程序。

#define InputSize 500000

int FindDuplicate::FindDuplicateNaive(int* input, int size)
{
    int j;
    for (int i = 0; i < size-1; i++)
    {
        for ( j= i+1; j < size; j++)
        {
            if (input[i] == input[j])
                return input[i];
        }
    }
    return -1;
}

int* FindDuplicate::CreateTestCase(int size)
{
    int* output = new int[size];
    int i;
    for ( i= 0; i < size-1; i++)
    {
        output[i] = i + 1;
    }
    output[i] = i;
    return output;
}

int main()
{

    int* input= FindDuplicate::CreateTestCase(InputSize);
    auto start = std::chrono::system_clock::now();//clock start 
    int output = FindDuplicate::FindDuplicateNaive(input, InputSize);
    auto end = std::chrono::system_clock::now();//clock end
    cout<<"Output is: "<<output<<endl;
    std::chrono::duration<double> elapsed_seconds = end - start;
    cout<< "elapsed time: " << elapsed_seconds.count() << "s\n";

}

这是Java程序...

public class FindDuplicate {

public static int FindDuplicateNaive(int[] input) {
    for (int i = 0; i < input.length - 1; i++) {
        for (int j = i + 1; j < input.length; j++) {
            if (input[i] == input[j])
                return input[i];
        }
    }
    return -1;
}

    public static int[] CreateTestCase(int n) {
    // 1, 2, 3, 4, 5, 1 = n = 6
    int[] output = new int[n];
    int i;
    for (i = 0; i < n - 1; i++) {
        output[i] = i + 1;
    }
    output[i] = i;
    return output;
}
    public static void main(String[] args) 
{
    //Here also args[0] is 5,00,000
    int number = Integer.parseInt(args[0]);

    int[] input = CreateTestCase(number);

    long start = System.currentTimeMillis();

    int output = FindDuplicateNaive(input);

    long end = System.currentTimeMillis();

    System.out.println("Total time taken is: " + (end - start) / 1000.0 + " secs");

    System.out.println(output);
}

你会震惊地知道同一个程序在 c++ 和 Java 中输入相同的输入所花费的时间。

在 Java 中:

总耗时:41.876 秒
499999

在 CPP 中:

启用优化后进入发布模式,

输出为:499999
经过时间:64.0293s

对此有任何想法,可能是什么原因?为什么 Java 需要 41.876 秒,而 CPP 需要 64.0293 秒?

【问题讨论】:

  • 评论不用于扩展讨论;这个对话是moved to chat
  • 一个(真正可以接受的)答案应该包括由 C++ 编译器生成的程序集 Hotspot JIT 生成的(最终的、优化的)结果。 (后者的相关摘录已包含在stackoverflow.com/a/56218736/3182664 中)

标签: java c++ performance visual-c++


【解决方案1】:

由于矢量化不容易发生,大部分时间都花在循环控制上。
由于在内部循环中使用了#pragma GCC unroll N,这有助于调查,循环展开提供了对 OP 结果的解释。

我获得了这些平均结果(控制台不包括在计时中):

gcc 8.3, -03, unroll 64    1.63s
gcc 8.3, -03, unroll 32    1.66s
gcc 8.3, -03, unroll 16    1.71s
gcc 8.3, -03, unroll 8     1.81s
gcc 8.3, -03, unroll 4     1.97s
gcc 8.3, -03, unroll 2     2.33s
gcc 8.3, -03, no unroll    3.06s
openjdk 10.0.2             1.93s

编辑:这些测试是在 InputSize=100'000 的情况下运行的,与原始问题一样(之后更改为 500'000)

【讨论】:

    【解决方案2】:

    主要区别在于循环展开。

    Java 非常巧妙地展开内部循环,而 GCC/clang/MSVC/ICC 不展开它(这是这些编译器错过的优化)。

    如果你手动展开循环,你可以将它加速到与 java 版本相似的速度,如下所示:

    for ( j= i+1; j < size-3; j+=4)
    {
        if (input[i] == input[j])
            return input[i];
        if (input[i] == input[j+1])
            return input[i];
        if (input[i] == input[j+2])
            return input[i];
        if (input[i] == input[j+3])
            return input[i];
    }
    for (; j < size; j++)
    {
        if (input[i] == input[j])
            return input[i];
    }
    

    为了证明,这里是java版本的内部循环(8x展开):

      0x00007f13a5113f60: mov     0x10(%rsi,%rdx,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f64: cmp     %ebx,%ecx
      0x00007f13a5113f66: je      0x7f13a5113fcb    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113f68: movsxd  %edx,%rdi
      0x00007f13a5113f6b: mov     0x14(%rsi,%rdi,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f6f: cmp     %ebx,%ecx
      0x00007f13a5113f71: je      0x7f13a5113fc9    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113f73: mov     0x18(%rsi,%rdi,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f77: cmp     %ebx,%ecx
      0x00007f13a5113f79: je      0x7f13a5113fed    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113f7b: mov     0x1c(%rsi,%rdi,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f7f: cmp     %ebx,%ecx
      0x00007f13a5113f81: je      0x7f13a5113ff2    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113f83: mov     0x20(%rsi,%rdi,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f87: cmp     %ebx,%ecx
      0x00007f13a5113f89: je      0x7f13a5113ff7    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113f8b: mov     0x24(%rsi,%rdi,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f8f: cmp     %ebx,%ecx
      0x00007f13a5113f91: je      0x7f13a5113ffc    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113f93: mov     0x28(%rsi,%rdi,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f97: cmp     %ebx,%ecx
      0x00007f13a5113f99: je      0x7f13a5114001    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113f9b: mov     0x2c(%rsi,%rdi,4),%ebx  ;*iaload
                                                    ; - FindDuplicate::FindDuplicateNaive@25 (line 6)
    
      0x00007f13a5113f9f: cmp     %ebx,%ecx
      0x00007f13a5113fa1: je      0x7f13a5114006    ;*if_icmpne
                                                    ; - FindDuplicate::FindDuplicateNaive@26 (line 6)
    
      0x00007f13a5113fa3: add     $0x8,%edx         ;*iinc
                                                    ; - FindDuplicate::FindDuplicateNaive@33 (line 5)
    
      0x00007f13a5113fa6: cmp     %r8d,%edx
      0x00007f13a5113fa9: jl      0x7f13a5113f60    ;*if_icmpge
                                                    ; - FindDuplicate::FindDuplicateNaive@17 (line 5)
    

    【讨论】:

      【解决方案3】:

      这不是一个完整的答案,我无法解释为什么它在 Java 中实际上比 C++ 运行得更快;但我可以解释一些阻碍你的 C++ 版本性能的事情。请不要选择此作为正确答案,以防有人对性能的总体差异有实际解释。

      这个答案已经在on meta 讨论过,并同意暂时将其作为部分答案是最好的选择。


      首先也是最重要的,正如 cmets 中的其他人所提到的,Java 代码在测试时已经优化,而在 C++ 中,您必须将优化级别指定为命令行参数(从 Visual Studio IDE 编译为发布版本),并且虽然这有很大的不同,但在我的测试中,Java 仍然居于首位(所有结果都在底部)。

      但我想指出您的测试中的一个主要缺陷,在这种特定情况下这似乎并不重要,因为当您查看数字时它几乎没有区别,但仍然很重要: 输入输出操作增加了明显的延迟。为了进行准确的执行时间比较,您必须从两种语言的计时器中排除输入输出操作。虽然在这种情况下差别不大,但让一种语言在计时器运行时同时执行函数和输出,而另一种语言只执行函数,这会使您的整个测试有偏差且毫无意义。

      要让它更等同于 Java 版本,请将您的 c++ main 更改为

      int main()
          {
          int* input = FindDuplicate::CreateTestCase(InputSize);   
      
          int result;
          auto start = std::chrono::system_clock::now(); //clock start 
          result = FindDuplicate::FindDuplicateNaive(input, InputSize);
          auto end = std::chrono::system_clock::now(); //clock end
      
          std::chrono::duration<double> elapsed_seconds = end - start;
          cout << "Output is: " << result << endl;
          cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
          }
      

      请注意,默认情况下,C++ 的控制台 I/O(iostream、cin/cout)甚至比它可能的要慢,因为启用了与 C 的控制台 I/O(stdio、scanf/printf)的同步以让程序无法执行如果同时使用 cout 和 printf 会发生奇怪的事情。 Here 你可以阅读关闭同步时 cout 的性能。您不仅在定时器约束中使用了 I/O,甚至还以最差的性能模式使用了它。

      这是我的结果,虽然仍然给 Java 带来了优势,但它显示了某些编译选项和 I/O 操作在 C++ 中可以产生多大的差异(对于单个 cout,通过关闭同步,平均 0.03 秒的差异大于它看起来)。 所有以秒为单位的值都是 10 次测试的平均值。

      1. Java print in timer                   1.52s
      2. Java                                  1.36s
      3. C++  debug, cout in timer            11.78s
      4. C++  debug                           11.73s
      5. C++  release, cout in timer           3.32s
      6. C++  release cout syncronization off  3.29s
      7. C++  release                          3.26s
      

      我想让您了解,在所有这些测试中,唯一有意义的比较是 1 与 62 与 7。无论您重复测试多少次,所有其他 (3, 4, 5) 都会进行有偏差的比较。

      【讨论】:

      • 恕我直言,您的答案不依赖于真实的工作台和测量结果,这对于性能问题来说是一个相当大的问题。顺便说一句,我自己做了测试,即使从时间测量中删除了 IO,也得到了与 OP 相同的结果,所以这个答案并不能真正回答这个问题。
      • 如果您使用编译器标志告诉 C++ 编译器使用 AVX2 指令生成代码(/arch:AVX2,可能使用 /favor:INTEL64)会发生什么?
      • @1201ProgramAlarm:AFAIK,只有 ICC 可以自动矢量化搜索循环,在第一次迭代之前无法计算行程计数。 gcc 和 clang 不能,IIRC MSVC 也不能。我很好奇 Java 实际上是什么机器代码 JITing,但是 SSE2 的自动矢量化可以很容易地解释它。 (如果输入适合 L2 缓存,我们可能会获得 4 倍的加速;但是 100k int32_t 大于典型 x86 CPU 上的 256kiB L2 缓存。或者如果 L2 缓存可以跟上,AVX2 可能会提高 8 倍。 ) 但无论如何,2 速度差异的因素可能是使用 SIMD 的 Java。
      • @1201ProgramAlarm:不,MSVC 不会自动矢量化,而且此代码甚至可以击败 ICC godbolt.org/z/JZIyxy。也许只是展开是 JVM 获胜的方式;我知道它会这样做(有时过于激进)。根据 Barnack 和 OP 正在测试的不同微架构,循环可能会成为分支吞吐量的瓶颈。顺便说一句,应该很容易用内在函数手动矢量化;您知道数组的末尾在哪里(因此不必担心越界读取),并且带有_mm_cmpeq_epi32_mm_movemask_epi8 的搜索循环是标准习惯用法。
      • 小注:int result = Find...可以做,不用先声明result再赋值。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-08-05
      • 2020-12-31
      • 2014-03-12
      • 2012-02-19
      • 2019-03-26
      • 1970-01-01
      相关资源
      最近更新 更多