最近我用C++编写了一个小程序(老实说,它更多的是C++类),并在Mac和Linux机器上测试了性能。
即使硬件是可比的,性能是如此的不同,我真的认为有一些奇怪的事情正在发生
首先是一些细节:
输入:约200MB压缩数据
程序的操作:它解压缩数据,然后将其加载到内存中,并执行许多数据访问以执行数据之间的连接。程序是顺序的(没有额外的线程或进程)。
输出:要在屏幕上显示的一些字符串
该代码在Linux机器中使用GCC 4.8.1编译,在Mac机器中使用GC 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)机器是一款macbook pro,配备了2.3 GHz Intel core I7(四核)256KB L2缓存、6MB L3缓存、8GB DDR3 1600Mhz和256GB SSD磁盘。
Linux机器(内核2.6.32-358)具有Intel E5-2620 2.0 GHz(六核)16MB缓存、64GB DDR3 1600Mhz和256GB SSD磁盘。两台机器都应该使用Sandy Bridge架构(也许Mac是常春藤桥,但无论如何,这应该不会有太大区别)。
现在,如果我在linux机器上启动程序,则需要217ms才能完成,而如果我在Mac机器上启动它,则需要132ms:这会使linux代码慢1.6倍!!
现在,我知道这两台机器有不同的操作系统和硬件,但我发现这种放缓太大了,无法用这些因素来证明,我觉得这背后一定有其他原因。
请注意,这个时间是在所有数据都加载到内存中之后进行的,我确信程序在此期间不会交换到磁盘。因此,我可以排除SSD磁盘的问题。
现在,我真的不知道是什么原因导致了这种放缓?内存基本上是相等的,而CPU只是慢了一点。
GCC在linux上产生的代码会比在mac上产生的更糟糕吗?
可能是Linux操作系统明显比Mac更糟糕吗?
我觉得这两件事都很难相信。有什么帮助吗?
编辑:
我意识到我没有提到我是如何计时的:好吧,我使用boost chrono库,我只测量调用主函数所需的时间。类似于:
time = now();
function();
duration = now() - time;
print(duration);
第2版:经过一些测试,我们用一个更简单(也很愚蠢)的程序:再现了性能的差异
#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 %ldn", 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速度相结合,即sandy和ivy bridge之间的差异,那么我们可能对这种差异有一个很好的解释。
不管怎样,谢谢大家的建议!
两个系统都遵循System V AMD64 ABI,所以gcc不应该在这方面有什么不同。不幸的是,如今系统性能中的随机效应相当普遍,因此有时你会通过重新排列链接顺序等愚蠢的事情来获得显著的性能差异(参见Mytkowicz等人,"在不做任何明显错误的事情的情况下产生错误的数据",http://citeseer.ist.psu.edu/viewdoc/summary?doi=10.1.1.163.8395)
以下是一些关于如何分析这一点的建议:
- 多跑一次。就我个人而言,我至少跑了11分,并比较了中位数(以及各种四分位数,但这可能比你关心的要多)。这样可以避免一些随机效应
- 将所有输出管道传输到一个文件中,以最大限度地减少UI效果
- 检查性能计数器。在Linux上,您可以使用
perf
工具。检查major-faults
,这表明您有需要转到磁盘的页面错误(当然,在多次运行时不太可能)。只有这样,您才能排除磁盘在这方面的作用。不幸的是,OS X(据我所知)没有一种简单的方法来收集性能计数器 - 您可以使用
-mcpu
进行实验,以强制执行相同的目标指令集 - 比较实际的缓存大小。
dmidecode -t cache
在Linux端可以做到这一点,但您必须是root用户。您的机器可能存在相关差异 - 如果您的程序运行了多个阶段,请尝试分别对它们进行基准测试
祝你好运!
从另一个角度来看,运行时差异仅为85毫秒,这是微小的。
你到底在测量什么?如果是整个程序运行时,包括启动和拆卸(例如使用Unix time
命令),那么差异可能很容易是由于所涉及的动态链接器:至少在Linux上,您的程序在实际执行之前会链接到系统libstdc++
。如果MacOS动态链接器稍微快一点(或者程序在Mac上静态链接?),这很容易解释差异。
或者甚至可能是写入终端所花费的时间。例如,在Linux上,gnome-terminal
经常被认为是"慢"的,因为它使用了抗锯齿字体和完全支持Unicode。如果你使用xterm
,你的程序运行得更快吗?如果将输出重定向到/dev/null
,会发生什么情况?
实际上,如果你考虑到不同的频率(如果你的程序是CPU绑定的,而不是内存绑定的,这可能很关键,你还没有告诉我们你的代码是做什么的),那么差异就会减少到~1.43。
然而,如果其中一个CPU是基于IvyBridge的,那么可能会有一些差异。诚然,体系结构并没有发生显著变化,但在对大量应用程序进行基准测试时,有些变化可能并不明显,但对特定应用程序可能至关重要。在你的例子中,你没有显示任何代码,但由于你处理的是大型内存结构,它可能与这些之一有关
- 此处描述的自适应填充策略
- 此处提到的动态预取限制
- 此处提到的下一页预取
关于实际实现的细节不多,但第一个实现的反向工程非常令人印象深刻,第二个和第三个名称不言自明(您可以通过禁用两台机器上的prefetces并再次进行比较来验证这是否是问题所在)。这些功能可能对一些消耗内存的工作负载(尤其是延迟关键的工作负载)非常关键,但如果不知道您对三级缓存的依赖程度,就很难判断
我还建议确保你不使用特定于操作系统的库版本或特定于编译器版本的内部函数,苹果公司的人可能在优化一些基本操作方面做得更好
我试图在Core 2 Duo PC上通过Linux Ubuntu编译该代码。我无法使rdtscp工作,而是使用了CPU计时器。该程序仅使用-O3选项进行编译。C程序的关键部分和程序集列表如下所示。这台电脑可以选择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