Fortran编译器真的好多少?


74

这个问题是对“ HPC的C ++ vs Fortran ”的答复中最近进行的两个讨论的扩展。而且,这不仅仅是一个挑战,而不是一个问题...

支持Fortran的最常听到的论据之一是编译器更好。由于大多数C / Fortran编译器共享相同的后端,因此为两种语言在语义上等效的程序生成的代码应该相同。但是,有人可能会争辩说,对于编译器来说,C / Fortran更容易实现优化。

因此,我决定尝试一个简单的测试:我获得了daxpy.fdaxpy.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所允许的),并且我一次处理两个向量。这是因为在许多体系结构中,乘法单元可能比矢量元素更多。

所有代码均使用gfortran / gcc 4.5版进行了编译,其标志为“ -O3 -Wall -msse2 -march = native -ffast-math -fomit-frame-pointer -malign-double -fstrict-aliasing”。在我的笔记本电脑(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中完成了,少了一点。

这比原始的C实现要慢一些,而Fortran的矢量实现要慢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的向量,如stali的原始版本一样)码)。由于现在的数字要小一些,因此我使用该选项-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的矩阵乘法示例,并将其添加到了网络上的文件中。这是一个和两个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则在几条指令中内联它们。

如果-fcx-limited-range设置了该选项,则C编译器将生成相同的紧凑代码,即,指示编译器忽略中间值中潜在的上溢/下溢。默认情况下,该选项在gfortran中已设置,可能会导致错误的结果。强制-fno-cx-limited-rangegfortran并没有改变任何东西。

因此,这实际上是反对使用gfortran进行数值计算的一个论点:即使正确的结果在浮点范围内,对复杂值的运算也可能溢出/溢出。这实际上是Fortran标准。在gcc或一般在C99中,除非另有说明,否则默认设置为严格执行操作(阅读IEEE-754兼容标准)。

提醒:请记住,主要问题是Fortran编译器是否产生比C编译器更好的代码。这里不是讨论一种语言胜过另一种语言的一般优点的地方。我真正感兴趣的是,如果有人可以找到一种方法,使用显式矢量化来哄骗gfortran来产生与C语言一样高效的daxpy,因为这例证了必须依靠编译器进行SIMD优化的问题,或者Fortran编译器胜过其C副本的情况。


一个时序问题是,如果您的处理器执行频率步进/涡轮模式,这些结果可能会出现在整个图上。
比尔·巴特

1
您的daxpy_c.c当前正在以x的倍数更新x,而根本没有触及y。您可能需要修复该问题以使其公平...
Jack Poulson

1
@JackPoulson:很好的收获,修复并更新了结果。
Pedro

2
另外,我相当确定差异完全是由于Fortran版本中的手动展开使编译器感到困惑。当我用与C版本中相同的简单循环替换它时,两者之间的性能几乎相同。如果不进行更改,则使用英特尔编译器的Fortran版本将变慢。
杰克·普尔森

1
@permeakra:实际上,C99标准指定了restrict关键字,该关键字准确地告诉编译器:假定数组不与任何其他数据结构重叠。
Pedro

Answers:


37

时间上的差异似乎是由于手动打开了跨步的 Fortran daxpy所致。使用以下命令,在2.67 GHz Xeon X5650上执行以下计时

./test 1000000 10000

英特尔11.1编译器

使用手动展开的Fortran:8.7秒不使用手动展开的
Fortran:5.8秒不使用手动展开的
C:5.8秒

GNU 4.1.2编译器

手动展开的Fortran:8.3秒手动展开的
Fortran(无手动):13.5秒
C 无需手动展开的Fortran :13.6秒(
带有矢量属性的C):5.8秒

GNU 4.4.5编译器

带有手动展开功能的Fortran:8.1秒不带手动展开功能的
Fortran:7.4秒
C不带手动展开功能的Fortran :8.5秒,
带矢量拐角处的温度:5.8秒

结论

  • 手动展开有助于在该体系结构上使用GNU 4.1.2 Fortran编译器,但会损害较新的版本(4.4.5)和Intel Fortran编译器。
  • 与版本4.2.1相比,GNU 4.4.5 C编译器在Fortran中更具竞争力。
  • 向量内在函数允许GCC性能与Intel编译器匹配。

是时候测试dgemv和dgemm等更复杂的例程了吗?


感谢您的结果!您使用的是什么版本的gcc,能否更具体地说明CPU?
2012年

2
您的编译器早于您的CPU ...您可以尝试使用gcc-4.5吗?
2012年

1
我刚试过 GCC 4.4.5的矢量化版本与Intel 11.1结果完全匹配。
Jack Poulson '02

1
我刚刚安装了gcc / gfortran版本4.4.5,但无法重现未展开的差异。实际上,在两种情况下生成的汇编程序中,最里面的循环是相同的,只是所使用的寄存器名称是可互换的。您可以确定要重新运行测试吗?
2012年

4
我们可以说这种解决了古老的争论“我们继续使用fortran是因为它的性能更高”,以便最终将其扔到垃圾箱中吗?
Stefano Borini,2012年

16

我参加这个聚会迟到了,所以我很难从头再来。问题很大,我想如果您有兴趣,可以将其分解为更小的部分。我感兴趣的一件事就是daxpy变量的性能,以及在这个非常简单的代码上,Fortran是否比C慢。

既可以在我的笔记本电脑(Macbook Pro,Intel Core i7,2.66 GHz)上运行,您的手动矢量化C版本和非手动矢量化Fortran版本的相对性能取决于所使用的编译器(具有您自己的选项):

Compiler     Fortran time     C time
GCC 4.6.1    5408.5 ms        5424.0 ms
GCC 4.5.3    7889.2 ms        5532.3 ms
GCC 4.4.6    7735.2 ms        5468.7 ms

因此,似乎GCC在4.6分支中的循环矢量化方面比以前更好。


在整个辩论中,我认为几乎可以像使用汇编语言一样,用C和Fortran编写快速且优化的代码。但是,我要指出一件事:就像汇编程序比C编写起来更乏味,但是可以让您更好地控制CPU执行的内容一样,C比Fortran更底层。因此,它使您可以更好地控制细节,从而有助于优化Fortran标准语法(或其供应商扩展)可能缺少功能的地方。一种情况是显式使用向量类型,另一种情况是可以手动指定变量的对齐方式,这是Fortran无法做到的。


欢迎来到scicomp!我同意在这种情况下,编译器版本与语言一样重要。在最后一句话中,您是说“ of”而不是“ off”吗?
阿隆·艾玛迪亚

9

我在Fortran中编写AXPY的方式略有不同。这是数学的确切翻译。

m_blas.f90

 module blas

   interface axpy
     module procedure saxpy,daxpy
   end interface

 contains

   subroutine daxpy(x,y,a)
     implicit none
     real(8) :: x(:),y(:),a
     y=a*x+y
   end subroutine daxpy

   subroutine saxpy(x,y,a)
     implicit none
     real(4) :: x(:),y(:),a
     y=a*x+y
   end subroutine saxpy

 end module blas

现在让我们在程序中调用上述例程。

测试f90

 program main

   use blas
   implicit none

   real(4), allocatable :: x(:),y(:)
   real(4) :: a
   integer :: n

   n=1000000000
   allocate(x(n),y(n))
   x=1.0
   y=2.0
   a=5.0
   call axpy(x,y,a)
   deallocate(x,y)

 end program main

现在让我们编译并运行它...

login1$ ifort -fast -parallel m_blas.f90 test.f90
ipo: remark #11000: performing multi-file optimizations
ipo: remark #11005: generating object file /tmp/ipo_iforttdqZSA.o

login1$ export OMP_NUM_THREADS=1
login1$ time ./a.out 
real    0 m 4.697 s
user    0 m 1.972 s
sys     0 m 2.548 s

login1$ export OMP_NUM_THREADS=2
login1$ time ./a.out 
real    0 m 2.657 s
user    0 m 2.060 s
sys     0 m 2.744 s

请注意,我没有使用任何循环或任何显式的OpenMP指令。在C语言中是否有可能(即不使用循环和自动并行化)?我不使用C,所以我不知道。


自动并行化是Intel编译器(Fortran和C)的功能,而不是该语言的功能。因此,C中的等效项也应并行化。只是出于好奇,它在n = 10000的情况下如何表现?
佩德罗(Pedro)

3
这就是重点。由于Fortran(与C不同)支持整个数组操作(如matmult,transpose等),因此在Fortran中,Autopar更加容易。因此,对于Fortran编译器而言,代码优化更加容易。GFortran(您已使用过)没有开发人员资源来优化Fortran编译器,因为他们目前的重点是实现Fortran 2003标准而不是优化。
stali 2012年

嗯... Intel C / C ++编译器icc也执行自动并行化。我已将文件添加icctest.c到其他来源。您可以使用与上面使用的选项相同的选项进行编译,运行并报告时间吗?我必须在代码中添加一个printf语句,以避免gcc优化所有内容。这只是一个快速的技巧,我希望它没有错误!
2012年

我已经下载了最新的icc / ifort编译器,并亲自进行了测试。该问题已被更新以包括这些新的结果,即英特尔的自动向量化两个Fortran和C.作品
佩德罗

1
谢谢。是的,我注意到差异不大,也许是因为循环简单并且操作是1级BLAS。但是正如我之前所说,由于Fortran能够执行整个数组操作并使用诸如PURE / ELEMENTAL之类的关键字,因此编译器优化的空间更大。编译器如何使用此信息以及其实际作用是另一回事。如果您想要bpaste.net/show/23035,
stali 2012年

6

我认为,编译器如何为现代硬件优化代码不仅很有趣。尤其是在GNU C和GNU Fortran之间,代码生成可能非常不同。

因此,让我们考虑另一个示例来显示它们之间的差异。

使用复数,GNU C编译器会为复数上几乎非常基本的算术运算产生大量开销。Fortran编译器提供了更好的代码。让我们看一下Fortran中的以下小示例:

COMPLEX*16 A,B,C
C=A*B

给出(gfortran -g -o complex.fo -c complex.f95; objdump -d -S complex.fo):

C=A*B
  52:   dd 45 e0                fldl   -0x20(%ebp)
  55:   dd 45 e8                fldl   -0x18(%ebp)
  58:   dd 45 d0                fldl   -0x30(%ebp)
  5b:   dd 45 d8                fldl   -0x28(%ebp)
  5e:   d9 c3                   fld    %st(3)
  60:   d8 ca                   fmul   %st(2),%st
  62:   d9 c3                   fld    %st(3)
  64:   d8 ca                   fmul   %st(2),%st
  66:   d9 ca                   fxch   %st(2)
  68:   de cd                   fmulp  %st,%st(5)
  6a:   d9 ca                   fxch   %st(2)
  6c:   de cb                   fmulp  %st,%st(3)
  6e:   de e9                   fsubrp %st,%st(1)
  70:   d9 c9                   fxch   %st(1)
  72:   de c2                   faddp  %st,%st(2)
  74:   dd 5d c0                fstpl  -0x40(%ebp)
  77:   dd 5d c8                fstpl  -0x38(%ebp)

这是39个字节的机器代码。当我们在C中考虑相同时

 double complex a,b,c; 
 c=a*b; 

并查看输出(以与上述相同的方式完成),我们得到:

  41:   8d 45 b8                lea    -0x48(%ebp),%eax
  44:   dd 5c 24 1c             fstpl  0x1c(%esp)
  48:   dd 5c 24 14             fstpl  0x14(%esp)
  4c:   dd 5c 24 0c             fstpl  0xc(%esp)
  50:   dd 5c 24 04             fstpl  0x4(%esp)
  54:   89 04 24                mov    %eax,(%esp)
  57:   e8 fc ff ff ff          call   58 <main+0x58>
  5c:   83 ec 04                sub    $0x4,%esp
  5f:   dd 45 b8                fldl   -0x48(%ebp)
  62:   dd 5d c8                fstpl  -0x38(%ebp)
  65:   dd 45 c0                fldl   -0x40(%ebp)
  68:   dd 5d d0                fstpl  -0x30(%ebp)

也是39字节的机器代码,但是功能步骤57引用了该代码,完成了工作的适当部分并执行了所需的操作。因此,我们有27字节的机器代码来运行多重操作。后面的功能是由muldc3提供的,libgcc_s.so并且在机器代码中占地1375字节。使用探查器时,这会大大降低代码速度,并提供有趣的输出。

当我们为实现上述BLAS示例zaxpy并执行相同的测试时,Fortran编译器应比C编译器提供更好的结果。

(我在实验中使用了GCC 4.4.3,但我注意到其他GCC发布了此行为。)

因此,在我看来,当我们考虑哪种是更好的编译器时,我们不仅要考虑并行化和矢量化,还必须研究如何将基本内容转换为汇编代码。如果此翻译给出了错误的代码,则优化只能将此内容用作输入。


1
我只是按照您的代码编写了一个示例,complex.c然后将其添加到在线代码中。我必须添加所有输入/输出以确保没有任何优化。__muldc3如果不使用,我只会打一个电话-ffast-math。有了-O2 -ffast-math9行嵌入式汇编程序。你能确认吗?
2012年

我发现了生成的汇编器存在差异的更具体原因,并将其添加到上面的问题中。
2012年

使用-O2会使编译器在运行时计算所有可能的结果,因此有时会丢失此类构造。如果要依赖输出,则不应在科学计算中使用-ffast-math选项。
MK又名Grisu,2012年

1
好吧,根据该参数(no -ffast-math),您不应将Fortran用于复杂值的计算。正如我在问题更新中所描述的那样,-ffast-math或更广泛地说,-fcx-limited-range是强制gcc使用与Fortran 中的标准相同的非IEEE受限范围计算。因此,如果您需要完整的复数值范围和正确的Inf和NaN,则不应该使用Fortran ...
Pedro

2
@Pedro:如果您希望GCC表现得像GFortran wrt。对于复杂的乘法和除法,应使用-fcx-fortran-rules。
janneb 2012年

4

民间,

我发现这个讨论非常有趣,但是令我惊讶的是,看到Matmul示例中的循环重新排序后,情况发生了变化。我当前的机器上没有intel编译器,所以我使用的是gfortran,但是将mm_test.f90中的循环重写为

call cpu_time(start)  
do r=1,runs  
  mat_c=0.0d0  
     do j=1,n  
        do k=1,n  
  do i=1,n  
           mat_c(i,j)=mat_c(i,j)+mat_a(i,k)*mat_b(k,j)  
        end do  
     end do  
  end do  
end do  
call cpu_time(finish)  

改变了我机器的整个结果。

先前版本的计时结果为:

#time ./mm_test_f 10000 100
 matmul    time   6.3620000000000001     
 triple do time   21.420999999999999     

而三重循环如上所示重新排列:

#time ./mm_test_f 10000 100
 matmul    time   6.3929999999999998     
 triple do time   3.9190000000000005    

这是Intel(R)CoreTM i7-2600K CPU @ 3.40GHz上的gcc / gfortran 4.7.2 20121109

使用的编译器标志来自我在此处获得的Makefile中的那些标志...


3
这并不奇怪,因为内存中的矩阵存储倾向于一个顺序,即,如果行是连续存储的,则最好循环遍历最内层的行,因为与重复加载(与)访问单个元素。请参阅stackoverflow.com/questions/7395556
克里斯蒂安·克拉森

我想我很惊讶“本征matmul”不会被编码为以这种方式执行操作。以第二种方式订购三重做的速度要快得多。看来确实是在此编译器集中,因为我可以得到的较早gfortran版本在其时序上更加“平稳”-不管用哪种方式进行复用-几乎都花了相同的时间。
沙特兹

-2

虽然它们可以提供帮助,但不是使代码运行更快的语言。使代码运行更快的是编译器,CPU和操作系统。比较语言只是一个误称,无用和无意义。这根本没有任何意义,因为您正在比较两个变量:语言和编译器。如果一个代码运行得更快,您将不知道该语言是多少,或者编译器是多少。我不明白为什么计算机科学界只是不明白这一点:-(

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.