【问题标题】:Huge performance difference of a C++ program (compiled with GCC) under Mac and LinuxMac 和 Linux 下 C++ 程序(用 GCC 编译)的巨大性能差异
【发布时间】:2013-12-09 20:53:07
【问题描述】:

最近我用 C++ 编写了一个小程序(好吧,说实话,它更多的是 C 加类),并在 Mac 和 Linux 机器上测试了性能。

尽管硬件相当,但性能与我的实际差别如此之大,发生了一些奇怪的事情。

首先是一些细节:

输入:约200MB压缩数据

程序的操作:对数据进行解压,然后加载到内存中,进行多次数据访问,实现数据之间的连接。该程序是顺序的(没有额外的线程或进程)。

输出:一些要显示在屏幕上的字符串

代码在 Linux 机器上使用 GCC 4.8.1 编译,在 Mac 机器上使用 GCC 4.8.2 编译。在这两种情况下,编译器都使用参数调用:

gcc -c -O3 -fPIC -MD -MF $(patsubst %.o,%.d,$@) //The last three arguments are to create the dependencies between the files

Mac(OS=mac mavericks 10.9)机器是配备 2.3 GHz Intel core I7(四核)256KB L2 缓存、6MB L3 缓存、8GB DDR3 1600Mhz 和 256 GB SSD 的 macbook pro磁盘。

Linux 机器(内核 2.6.32-358)具有 Intel E5-2620 2.0 GHz(六核)16MB 缓存、64GB DDR3 1600Mhz 和 256GB SSD 磁盘。两台机器都应该使用 Sandy Bridge 架构(也许 Mac 是 ivy bridge,但无论如何这应该不会有太大区别)。

现在,如果我在 linux 机器上启动程序,则需要 217 毫秒才能完成,而如果我在 Mac 机器上启动它需要 132 毫秒:这会使 linux 代码慢 1.6 倍!

现在,我了解到两台机器的操作系统和硬件不同,但我发现这样的减速太大了,这些因素无法证明是合理的,我觉得这背后一定有其他原因。

请注意,此时间是在所有数据加载到内存后进行的,我确信程序在此期间不会交换到磁盘。因此,我可以排除SSD磁盘的问题。

现在,我真的不知道是什么导致了这种放缓?内存基本等价,CPU只是慢了一点。

难道 GCC 在 linux 上生成的代码比在 mac 上更糟糕吗?

会不会是 Linux 操作系统明显比 Mac 差?

我觉得这两件事都难以置信。有什么帮助吗?

编辑:

我意识到我没有提到我是如何进行计时的:嗯,我使用 boost chrono 库,并且我只测量调用 main 函数所需的时间。比如:

time = now();
function();
duration = now() - time;
print(duration);

EDIT2: 经过一些测试,我们设法用一个更简单(也很愚蠢)的程序重现了性能差异:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

char in1[10000000];
char in2[10000000];

static inline uint64_t rdtscp (void) {
    uint64_t low, high;
    uint64_t aux;

    __asm__ __volatile__ (
                    ".byte 0x0f,0x01,0xf9"
                    : "=a" (low), "=d" (high), "=c" (aux)
                    );

    return low | (high << 32);
}

int main(int argc, char** argv) {

    uint64_t counter = rdtscp();

    for(int i = 0; i < 10000000; ++i) {
            in1[i] = (char)i * 200;
            in2[i] = (char)i * 100;
    }

    int joins = 0;
    for(int j = 0; j < 10000000; ++j) {
            int el = in1[j];
            for(int m = 0; m < 10000000; m++) {
                    if (in2[m] == el) {
                            joins++;
                            break;
                    }
            }
    }
    printf("Joins %d Cycles total %ld\n", joins, (rdtscp() - counter));

    return 0;
}

请不要看程序的操作。他们没有什么意义。我们试图重现的是一系列对内存的访问和对它们的简单操作。

我们在 Mac 上启动了这个程序,输出结果是:

Joins 10000000 Cycles total 589015641

在 linux 机器上是:

Joins 10000000 Cycles total 838198832

显然,linux 版本需要更多的 CPU 周期,这可能是访问内存所必需的。现在的问题是:为什么内存访问变慢了?

一个原因可能是 in1 和 in2 不适合 CPU 缓存,这需要一些 RAM 访问。正如 Roy Longbottom 所指出的,linux 中的内存确实是 ECC,这可能是性能较低的原因。如果我们将其与略低的 CPU 速度、沙桥和常春藤桥之间的差异结合起来,那么我们可能对这种差异有一个很好的解释。

无论如何,谢谢大家的提示!

【问题讨论】:

  • 你是如何安排程序的?启动/完成或进入/退出main? (应该是后者)
  • “some”有多少个字符串?如果它不止几个,您可以尝试将输出重定向到一个文件,以防输出减慢速度。不过老实说,一个比另一个少花一点点时间可能不值得太担心。如果您需要扩展到更大的数据集或更长的工作时间,那么可能值得对两者进行分析,看看是否可以缩小范围。
  • 我的代码几乎有 2 倍的差异(有利于在 Mac 上编译和运行的代码)。我尝试在非常不同的硬件上在 linux(Ubuntu、CentOS)下编译和运行我的 c++ 代码,并且在所有情况下,Linux 的速度都慢了大约 2 倍。使用完全相同的优化标志和 g++ 编译器

标签: c++ performance macos gcc operating-system


【解决方案1】:

这两个系统都遵循 System V AMD64 ABI,所以 gcc 应该不会有什么不同。不幸的是,现在系统性能中的随机效应相当普遍,因此您有时可以通过像重新排序链接顺序这样愚蠢的事情来获得显着的性能差异(参见 Mytkowicz 等人,“产生错误的数据而不做任何明显错误的事情”) , http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.163.8395)

下面是一些关于如何分析这个问题的建议:

  1. 运行不止一次。就我个人而言,我至少跑了 11 次并比较了中位数(以及各个四分位数,但这可能比你可能关心的要多)。这样可以避免一些随机效应。
  2. 将所有输出通过管道传输到一个文件中以最小化 UI 效果。
  3. 检查您的性能计数器。在 Linux 上,您可以使用perf' tool. Check formajor-faults',这表明您有需要转到磁盘的页面错误(当然,不太可能在多次运行时)。只有这样,您才能排除磁盘在那里有所作为。不幸的是,OS X(据我所知)没有这么简单的方法来收集性能计数器。
  4. 您可以尝试使用 `-mcpu' 来强制使用相同的目标指令集。
  5. 比较实际缓存大小。 `dmidecode -t cache' 在 Linux 端执行此操作,但您必须是 root。您的机器可能存在相关差异。
  6. 如果您的程序运行多个阶段,请尝试分别对它们进行基准测试。

祝你好运!

【讨论】:

    【解决方案2】:

    换个角度看,运行时差只有85毫秒,很小

    您具体测量的是什么?如果它是整个程序运行时,包括启动和拆卸(例如使用 Unix time 命令),那么差异可能很容易归因于所涉及的动态链接器:至少在 Linux 上,您的程序将链接到系统 libstdc++ 之前它实际上已执行。如果 MacOS 动态链接器稍微快一点(或者程序在 Mac 上静态链接?),这很容易解释差异。

    或者甚至可能是写入终端所花费的时间。例如,在 Linux 上,gnome-terminal 经常被视为“慢”,因为它使用了抗锯齿字体和完整的 Unicode 支持。如果您改用xterm,您的程序会运行得更快吗?如果将输出重定向到/dev/null,会发生什么?

    【讨论】:

      【解决方案3】:

      实际上,如果您考虑到不同的频率(如果您的程序受 CPU 限制而不是内存限制,这可能很关键,您还没有告诉我们您的代码做了什么),那么差异会减少到 ~1.43 .

      但是,如果其中一个 CPU 是基于 IvyBridge 的,则可能存在相当大的差异。确实,架构并没有发生显着变化,但是在对大量应用程序进行基准测试时,有些变化可能并不明显,但对特定应用程序可能很关键。在您的情况下,您没有显示任何代码,但由于您正在处理大型内存结构,它可能与其中之一有关

      • 自适应填充策略描述here
      • 动态预取限制,提到了here
      • 下一页预取,提到here

      关于实际实现的细节并不多,但在第一个上完成的逆向工程令人印象深刻,第二个和第三个名字不言自明(您可以通过禁用两台机器上的首选项来验证这是否是问题所在再次比较)。这些功能对于一些消耗内存的工作负载(尤其是延迟关键的工作负载)可能非常关键,但如果不知道您对 L3 缓存的依赖程度,就很难判断

      我还建议确保您不要使用特定于操作系统的库版本或特定于编译器版本的内在函数,Apple 人员可能在优化一些基本操作方面做得更好

      【讨论】:

        【解决方案4】:

        我尝试在我的 Core 2 Duo PC 上通过 Linux Ubuntu 编译该代码。我无法让 rdtscp 工作,而是使用了 CPU 时间计数器。该程序仅使用 -O3 选项编译。 C 程序的关键部分和汇编清单如下所示。此 PC 可以选择 2.4 GHz 或 1.6 GHz 并默认按需生成不同的性能(在 1.6 和 2.4 GHz 之间)。 1.6 和 2.4 GHz 的结果如下所示。我添加了额外的计数(浮点数),以发现发生了什么。然后每秒连接的速度没有什么不同。

        每秒连接数的结果与 CPU MHz 成正比,如果与主内存速度有关,则不太可能。将数组和循环计数分别增加 10 和 100 倍会产生相同的每秒连接数,这表明内存速度可以忽略不计。

        因此,在 Turbo Boost 下,我们只剩下相对 GHz、生成相同的机器代码(通知对齐)以及 Sandy Bridge 与 Ivy Bridge 的效果。使用额外的计数器,可以计算执行的汇编指令的数量 - 我在计数时迷路了。

            for(j = 0; j < 10000000; ++j) {
                    int el = in1[j];
                    for(m = 0; m < 10000000; m++) {
                            count = count + 1;
                            if (in2[m] == el) 
                            {
                                    joins++;
                                    break;
                            }
                    }
            }
        
          .L6:
                movzbl  in1(%ecx), %edx
                xorl    %eax, %eax
                jmp     .L5
                .p2align 4,,7
                .p2align 3
          .L3:
                addl    $1, %eax
                cmpl    $10000000, %eax
                je      .L4
          .L5:
                cmpb    in2(%eax), %dl
                fadd    %st, %st(1)
                jne     .L3
                addl    $1, %ebx
          .L4:
                addl    $1, %ecx
                cmpl    $10000000, %ecx
                jne     .L6
        
         Result 2400 MHz
         Count  320000000  Joins 10000000  0.4920310 seconds  20.32M  joins per second
         Result 1600 MHz
         Count  320000000  Joins 10000000  0.7400470 seconds  13.51M  joins per second
        

        【讨论】:

          猜你喜欢
          • 2019-03-26
          • 2017-05-08
          • 2010-10-15
          • 2013-02-18
          • 1970-01-01
          • 1970-01-01
          • 2019-04-21
          • 2017-03-24
          • 1970-01-01
          相关资源
          最近更新 更多