这个问题是最近在“ C++ vs Fortran for HPC ”的回复中提出的两个讨论的延伸。这更像是一个挑战而不是一个问题......
支持 Fortran 的最常听到的论点之一是编译器更好。由于大多数 C/Fortran 编译器共享相同的后端,因此为两种语言的语义等效程序生成的代码应该相同。然而,有人可能会争辩说,C/Fortran 对编译器来说更容易优化。
所以我决定尝试一个简单的测试:我得到一份daxpy.f和daxpy.c的副本,并用 gfortran/gcc 编译它们。
现在 daxpy.c 只是 daxpy.f 的 f2c 翻译(自动生成的代码,丑陋),所以我拿了那个代码并清理了一点(见 daxpy_c),这基本上意味着将最里面的循环重写为
for ( i = 0 ; i < n ; i++ )
dy[i] += da * dx[i];
最后,我使用 gcc 的向量语法重写了它(输入 daxpy_cvec):
#define vector(elcount, type) __attribute__((vector_size((elcount)*sizeof(type)))) type
vector(2,double) va = { da , da }, *vx, *vy;
vx = (void *)dx; vy = (void *)dy;
for ( i = 0 ; i < (n/2 & ~1) ; i += 2 ) {
vy[i] += va * vx[i];
vy[i+1] += va * vx[i+1];
}
for ( i = n & ~3 ; i < n ; i++ )
dy[i] += da * dx[i];
请注意,我使用长度为 2 的向量(这是 SSE2 允许的所有向量)并且我一次处理两个向量。这是因为在许多架构上,我们可能拥有比向量元素更多的乘法单元。
所有代码均使用带有标志“-O3 -Wall -msse2 -march=native -ffast-math -fomit-frame-pointer -malign-double -fstrict-aliasing”的 gfortran/gcc 4.5 版编译。在我的笔记本电脑(Intel Core i5 CPU,M560,2.67GHz)上,我得到以下输出:
pedro@laika:~/work/fvsc$ ./test 1000000 10000
timing 1000000 runs with a vector of length 10000.
daxpy_f took 8156.7 ms.
daxpy_f2c took 10568.1 ms.
daxpy_c took 7912.8 ms.
daxpy_cvec took 5670.8 ms.
所以原始的 Fortran 代码需要 8.1 秒多一点,其自动翻译需要 10.5 秒,天真的 C 实现在 7.9 中完成,而显式矢量化代码在 5.6 中完成,略少。
这就是 Fortran 比简单的 C 实现稍慢,比矢量化 C 实现慢 50%。
所以这里有一个问题:我是一个本地 C 程序员,所以我很有信心我在那个代码上做得很好,但是 Fortran 代码最后一次被触及是在 1993 年,因此可能有点过时了。由于我不像这里的其他人那样在 Fortran 中编码感到自在,任何人都可以做得更好,即与两个 C 版本中的任何一个相比更具竞争力吗?
另外,任何人都可以使用 icc/ifort 进行此测试吗?矢量语法可能不起作用,但我很想知道天真的 C 版本在那里的行为。周围有 xlc/xlf 的任何人也是如此。
我在这里上传了源代码和 Makefile 。要获得准确的计时,请将 test.c 中的 CPU_TPS 设置为 CPU 上的 Hz 数。如果您发现任何版本的任何改进,请在此处发布!
更新:
我已经将 stali 的测试代码添加到在线文件中,并用 C 版本对其进行了补充。我修改了程序以在长度为 10'000 的向量上执行 1'000'000 循环,以与之前的测试一致(并且因为我的机器无法分配长度为 1'000'000'000 的向量,就像在 sali 的原始代码)。由于数字现在有点小,我使用该选项-par-threshold:50
使编译器更有可能并行化。使用的icc/ifort版本是12.1.2 20111128,结果如下
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./icctest_c
3.27user 0.00system 0:03.27elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./icctest_f
3.29user 0.00system 0:03.29elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./icctest_c
4.89user 0.00system 0:02.60elapsed 188%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./icctest_f
4.91user 0.00system 0:02.60elapsed 188%CPU
总之,就所有实际目的而言,C 和 Fortran 版本的结果是相同的,并且两个代码都自动并行化。请注意,与之前的测试相比,速度更快是由于使用了单精度浮点运算!
更新:
虽然我不太喜欢举证责任在哪里,但我已经用 C 重新编码了 stali 的矩阵乘法示例并将其添加到web上的文件中。以下是一个和两个 CPU 的三重循环的结果:
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_f 2500
triple do time 3.46421700000000
3.63user 0.06system 0:03.70elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_c 2500
triple do time 3.431997791385768
3.58user 0.10system 0:03.69elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_f 2500
triple do time 5.09631900000000
5.26user 0.06system 0:02.81elapsed 189%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_c 2500
triple do time 2.298916975280899
4.78user 0.08system 0:02.62elapsed 184%CPU
请注意,cpu_time
在 Fortran 中测量的是 CPU 时间而不是挂钟时间,因此我将调用包装起来time
以比较 2 个 CPU 的情况。结果之间没有真正的区别,只是 C 版本在两个内核上的表现要好一些。
现在对于matmul
命令,当然仅在 Fortran 中,因为此内在函数在 C 中不可用:
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_f 2500
matmul time 23.6494780000000
23.80user 0.08system 0:23.91elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_f 2500
matmul time 26.6176640000000
26.75user 0.10system 0:13.62elapsed 197%CPU
哇。这绝对是可怕的。谁能找出我做错了什么,或者解释为什么这个内在仍然是一件好事?
我没有将dgemm
调用添加到基准测试中,因为它们是对英特尔 MKL 中相同函数的库调用。
对于未来的测试,任何人都可以提出一个已知在 C 中比在 Fortran 中慢的示例吗?
更新
为了验证 stali 的说法,即matmul
内在函数在较小矩阵上比显式矩阵乘积快“一个数量级”,我修改了他自己的代码,使用这两种方法将大小为 100x100 的矩阵相乘,每种方法 10'000 次。一个和两个 CPU 上的结果如下:
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=1 time ./mm_test_f 10000 100
matmul time 3.61222500000000
triple do time 3.54022200000000
7.15user 0.00system 0:07.16elapsed 99%CPU
pedro@laika:~/work/fvsc$ OMP_NUM_THREADS=2 time ./mm_test_f 10000 100
matmul time 4.54428400000000
triple do time 4.31626900000000
8.86user 0.00system 0:04.60elapsed 192%CPU
更新
Grisu 正确地指出,在没有优化的情况下,gcc 将复数运算转换为库函数调用,而 gfortran 将它们内联在几条指令中。
如果设置了该选项,C 编译器将生成相同的紧凑代码-fcx-limited-range
,即指示编译器忽略中间值中潜在的上溢/下溢。此选项在 gfortran 中默认以某种方式设置,可能会导致不正确的结果。强制-fno-cx-limited-range
gfortran 并没有改变任何东西。
所以这实际上是反对使用 gfortran 进行数值计算的一个论点:即使正确的结果在浮点范围内,对复数值的操作也可能溢出/下溢。这实际上是一个 Fortran 标准。在 gcc 或一般的 C99 中,除非另有说明,否则默认是严格执行操作(阅读 IEEE-754 兼容)。
提醒:请记住,主要问题是 Fortran 编译器是否比 C 编译器生成更好的代码。这里不是讨论一种语言相对于另一种语言的一般优点的地方。我真正感兴趣的是,是否有人可以找到一种方法来哄骗 gfortran 使用显式矢量化生成与 C 中的 daxpy 一样高效,因为这说明了必须专门依赖编译器进行 SIMD 优化的问题,或者Fortran 编译器胜过 C 对应的情况。