GCC编译速度非常慢(大文件)

9
我正在尝试编译一个大型的C文件(特别是为了MATLAB mexing)。这个C文件大约有20MB(如果您想玩一下,可以从GCC错误跟踪器下载)。
以下是我运行的命令和屏幕输出。这已经运行了几个小时了,正如您所看到的,优化已经被禁用(-O0)。为什么这么慢?有没有办法让它更快?
(参考资料:Ubuntu 12.04(Precise Pangolin)64位和GCC 4.7.3)
/usr/bin/gcc -c -DMX_COMPAT_32   -D_GNU_SOURCE -DMATLAB_MEX_FILE  -I"/usr/local/MATLAB/R2015a/extern/include" -I"/usr/local/MATLAB/R2015a/simulink/include" -ansi -fexceptions -fPIC -fno-omit-frame-pointer -pthread -O0 -DNDEBUG path/to/test4.c -o /tmp/mex_198714460457975_3922/test4.o -v
Using built-in specs.
COLLECT_GCC=/usr/bin/gcc
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu/Linaro 4.7.3-2ubuntu1~12.04' --with-bugurl=file:///usr/share/doc/gcc-4.7/README.Bugs --enable-languages=c,c++,go,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-4.7 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --with-gxx-include-dir=/usr/include/c++/4.7 --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --enable-gnu-unique-object --enable-plugin --with-system-zlib --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64 --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 4.7.3 (Ubuntu/Linaro 4.7.3-2ubuntu1~12.04)
COLLECT_GCC_OPTIONS='-c' '-D' 'MX_COMPAT_32' '-D' '_GNU_SOURCE' '-D' 'MATLAB_MEX_FILE' '-I' '/usr/local/MATLAB/R2015a/extern/include' '-I' '/usr/local/MATLAB/R2015a/simulink/include' '-ansi' '-fexceptions' '-fPIC' '-fno-omit-frame-pointer' '-pthread' '-O0' '-D' 'NDEBUG' '-o' '/tmp/mex_198714460457975_3922/test4.o' '-v' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/4.7/cc1 -quiet -v -I /usr/local/MATLAB/R2015a/extern/include -I /usr/local/MATLAB/R2015a/simulink/include -imultilib . -imultiarch x86_64-linux-gnu -D_REENTRANT -D MX_COMPAT_32 -D _GNU_SOURCE -D MATLAB_MEX_FILE -D NDEBUG path/to/test4.c -quiet -dumpbase test4.c -mtune=generic -march=x86-64 -auxbase-strip /tmp/mex_198714460457975_3922/test4.o -O0 -ansi -version -fexceptions -fPIC -fno-omit-frame-pointer -fstack-protector -o /tmp/ccxDOA5f.s
GNU C (Ubuntu/Linaro 4.7.3-2ubuntu1~12.04) version 4.7.3 (x86_64-linux-gnu)
    compiled by GNU C version 4.7.3, GMP version 5.0.2, MPFR version 3.1.0-p3, MPC version 0.9
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/4.7/../../../../x86_64-linux-gnu/include"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/MATLAB/R2015a/extern/include
 /usr/local/MATLAB/R2015a/simulink/include
 /usr/lib/gcc/x86_64-linux-gnu/4.7/include
 /usr/local/include
 /usr/lib/gcc/x86_64-linux-gnu/4.7/include-fixed
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.
GNU C (Ubuntu/Linaro 4.7.3-2ubuntu1~12.04) version 4.7.3 (x86_64-linux-gnu)
    compiled by GNU C version 4.7.3, GMP version 5.0.2, MPFR version 3.1.0-p3, MPC version 0.9
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: c119948b394d79ea05b6b3986ab084cf

编辑:接下来是:我遵循了chqrlie的建议,并在<5秒内tcc编译了我的函数(我只需要删除-ansi标志并将"gcc"改为"tcc"),这真的相当不错。我只能想象GCC的复杂性。

然而,在尝试mex它时,通常还需要一个其他命令。第二个命令通常是:

/usr/bin/gcc -pthread -Wl,--no-undefined -Wl,-rpath-link,/usr/local/MATLAB/R2015a/bin/glnxa64 -shared  -O -Wl,--version-script,"/usr/local/MATLAB/R2015a/extern/lib/glnxa64/mexFunction.map" /tmp/mex_61853296369424_4031/test4.o   -L"/usr/local/MATLAB/R2015a/bin/glnxa64" -lmx -lmex -lmat -lm -lstdc++ -o test4.mexa64

由于一些标志不兼容,我无法使用tcc来运行这个。如果我尝试使用GCC运行第二个编译步骤,则会出现以下错误:

/usr/bin/ld: test4.o: relocation R_X86_64_PC32 against undefined symbol `mxGetPr' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Bad value
collect2: error: ld returned 1 exit status

修改:解决方法似乎是使用clang编译器。tcc可以编译文件,但在mexing的第二步中使用的参数与tcc的参数选项不兼容。Clang非常快速,生成一个漂亮,小巧,优化的文件。


5
这段C语言代码非常奇怪。您能否考虑生成不同的C代码? - fuz
16
我不会购买一个价值2000美元的软件包,只是为了在Stack Overflow上回答某人的问题。如果你需要帮助,惯例是提供一个自完整的示例。因为你的代码需要使用专有的mex.h头文件,所以它并不是自完整的。一个20 MiB的C表达式肯定很奇怪。也许可以用不同的方式表达相同的概念,比如使用从数组中提取的参数进行循环。 - fuz
6
@user650261请引用你所说的“大公司编制大文件”的来源。我敢打赌一杯咖啡,他们会将它们分成更小的可管理模块,因为他们肯定知道“普遍信仰”。 - Weather Vane
7
我不会从可能不可靠的外部来源下载源代码。但我支持FUZxxl和WeatherVane的观点。如此大的文件明显是异常的,不仅难以维护,而且编辑和调试也是一场噩梦。@FUZxxl的评论很合理,你没有权利要求他闭嘴。请求帮助的是,显然你对文件大小有问题。尝试将其分解成更小的单元。 - too honest for this site
4
好的,我同意文件大小是这里固有的问题,并且其他人可能也会遇到类似的问题,所以我重新打开了它。然而,我担心链接的文件在未来的某个时候会消失,使得这个问题变得不太有用。这就是为什么我们建议人们将此问题最小化并将其放在问题本身中。这是一个边缘案例。我只是要求每个人保持评论得体并与主题相关。 - Brad Larson
显示剩余21条评论
3个回答

17
几乎整个文件只有一个表达式,即double f[24] = ...的赋值。这将生成一个巨大的抽象语法树。除非使用专门的编译器,否则很难高效处理。
文件本身可能没有问题,但一个巨大的表达式可能是引起问题的原因。作为预处理步骤,尝试将该行拆分为double f[24] = {0},再进行24个赋值f[0] = ...; f[1] = ...,看看会发生什么。最坏情况下,您可以将24个赋值拆分为24个函数,每个函数在其自己的.c文件中,并单独编译它们。这不会减少AST的大小,只是重新组织了它,但GCC可能更优化于处理许多语句,这些语句加起来构成了大量代码,而不是一个巨大的表达式。
最终的方法是以更优化的方式生成代码。例如,如果我搜索s4*s5*s6,得到77,783个结果。这些变量s[4-6]不会改变。你应该生成一个临时变量,double _tmp1 = s4*s5*s6;,然后使用它代替重复的表达式。这样就从抽象语法树中消除了311,132个节点(假设s4*s5*s6是5个节点,_tmp1是一个节点)。这将减少GCC的处理量。这还应该生成更快的代码(您不必重复77,783次相同的乘法)。

如果您以递归的方式以聪明的方式执行此操作(例如s4 * s5 * s6 - > _tmp1(c4 * c6 + s4 * s5 * s6) - > (c4 * c6 + _tmp1) - > _tmp2c5 * s6 *(c4 * c6 + s4 * s5 * s6) - > c5 * s6 * _tmp2 - > _tmp3),您可能可以消除大部分生成代码的大小。


8
我会指出C11草案标准中的5.2.4.1翻译限制,第1节[...]逻辑源代码行中4095个字符[...]。如果这是一个单一的20MB表达式,编译器可能根本无法编译它。 - EOF
@EOF Op可以将表达式分成多行以规避此限制,但他的代码违反了其他限制。 - fuz
谢谢您的有益回复。我考虑过通过预计算来简化它 - 这是我计划采取的后续步骤,但如果这样可以解决问题,我现在可能会这样做。我想知道您是否能更多地谈论解析树机制 - 为什么解析需要这么长时间?我注意到编译可以非常快速地检测到语法错误,那么为什么在这种情况下进行编译的解析需要这么长时间。对于这些问题我很抱歉 - 我只是想了解是什么原因导致了问题,以便我不会走进一些无用的改变中。 - user650261
@user650261 gcc正在对解析树进行一些转换(特别是寄存器分配)。其中一些转换可能具有比O(n)更差的运行时,使它们在大型树上变得非常缓慢。由于表达式几乎永远不会超过约1000个字符,因此这并不太重要,但是当您提供一个比实际代码的上限大20000倍的表达式时,情况就不同了。 - fuz
2
@user650261:嗯,我可能错了,可能不是解析需要那么长时间。我在文件末尾放了一个解析错误,它在几秒钟内就捕获到了。除非它在首先进行解析之前就为捕获这些错误进行了优化,但我怀疑这一点。正如FUZxxl所说,可能是对解析树的转换占用了太多时间。 - Claudiu
4
根据我的实验,gcc的解析阶段大约需要10秒钟,解析树分析和代码生成必须使用高于O(n)复杂度的算法,可能是O(n*2)甚至更糟糕,这需要数小时才能完成。tcc不会建立解析树,在单个步骤中即时生成代码。输出非常大(42MB的代码+数据),但它能够快速完成,即使38MB的迭代代码也应该在相当短的时间内执行,远远不到1秒钟。 - chqrlie

15

经过测试,我发现Clang编译器在编译大文件时似乎出现的问题较少。尽管在编译期间,Clang消耗了近1GB的内存,但它成功地将OP的源代码转化为一个70kB的目标文件。这对我所测试的所有优化级别都有效。

如果打开优化功能,gcc也能够快速编译该文件,并且不会占用过多的内存。这个gcc bug来自于OP代码中的大表达式,它给寄存器分配器带来了巨大的负担。打开优化功能后,编译器执行一种叫做公共子表达式消除的优化,它能够从OP代码中删除许多冗余部分,从而减少编译时间和目标文件大小,使其变得更加可控。

以下是一些针对上述bug报告中的测试用例的测试结果:

$ time gcc5 -O3 -c -o testcase.gcc5-O3.o testcase.c
real    0m39,30s
user    0m37,85s
sys     0m1,42s
$ time gcc5 -O0 -c -o testcase.gcc5-O0.o testcase.c
real    23m33,34s
user    23m27,07s
sys     0m5,92s
$ time tcc -c -o testcase.tcc.o testcase.c
real    0m2,60s
user    0m2,42s
sys     0m0,17s
$ time clang -O3 -c -o testcase.clang-O3.o testcase.c
real    0m13,71s
user    0m12,55s
sys     0m1,16s
$ time clang -O0 -c -o testcase.clang-O0.o testcase.c
real    0m17,63s
user    0m16,14s
sys     0m1,49s
$ time clang -Os -c -o testcase.clang-Os.o testcase.c
real    0m14,88s
user    0m13,73s
sys 0m1,11s
$ time clang -Oz -c -o testcase.clang-Oz.o testcase.c
real    0m13,56s
user    0m12,45s
sys     0m1,09

这是生成的目标文件大小:

    text       data     bss      dec        hex filename
39101286          0       0 39101286    254a366 testcase.clang-O0.o
   72161          0       0    72161      119e1 testcase.clang-O3.o
   72087          0       0    72087      11997 testcase.clang-Os.o
   72087          0       0    72087      11997 testcase.clang-Oz.o
38683240          0       0 38683240    24e4268 testcase.gcc5-O0.o
   87500          0       0    87500      155cc testcase.gcc5-O3.o
   78239          0       0    78239      1319f testcase.gcc5-Os.o
69210504    3170616       0 72381120    45072c0 testcase.tcc.o

6

试试Fabrice Bellard的小型C编译器tcc,网址为http://tinycc.org:

chqrlie$ time tcc -c test4.c

real    0m1.336s
user    0m1.248s
sys     0m0.084s

chqrlie$ size test4.o
   text    data     bss     dec     hex filename
38953877        3170632       0 42124509        282c4dd test4.o

是的,在一台相当基础的PC上只需要1.336秒

当然,我无法测试生成的可执行文件,但目标文件应能与您的程序和库链接。

为了进行此测试,我使用了文件mex.h的虚拟版本:

typedef struct mxArray mxArray;
double *mxGetPr(const mxArray*);
enum { mxREAL = 0 };
mxArray *mxCreateDoubleMatrix(int nx, int ny, int type);

gcc 仍未完成编译...

编辑:gcc 能够极大地占用我的Linux计算机,导致我不能再连接:(


如果我可以问一个快速的跟进问题:由于您似乎有tcc的经验,您知道添加外部mex.h文件的正确语法吗?我阅读了文档并尝试了:'tcc test4.c -Idir"/usr/local/MATLAB/R2015a/extern/include" -Idir"/usr/local/MATLAB/R2015a/simulink/include"',但不幸的是,这返回了“test4.c:1: error: include file 'mex.h' not found”,即使它在那个目录中。 - user650261
在选项中删除 dirtcc test4.c -I"/usr/local/MATLAB/R2015a/extern/include" -I"/usr/local/MATLAB/R2015a/simulink/include"。这些选项与 gcc 相同。 - chqrlie
1
@user650261,tcc 的命令行语法大部分与 gcc 相同。 - fuz

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