Fortran是一门古老的适用于数值计算的高级语言,有着悠久的历史。1981年以前,Fortran是世界上最流行的编程语言。各个领域的科学家,工程人员编写了不计其数的Fortran代码用于计算航空,大坝,大气等领域的科学计算。在知名科幻小说三体中,叶文洁第一次进入红岸基地,就对Fortran语言产生了兴趣:叶文洁第一次走进主机房时,看到一排阴极射线管显示屏,她惊奇地发现,屏幕上竟滚动着排排程序代码,可以通过键盘随意-进行编辑和调试。而她在大学里使用计算机时,代码都写在一张张打格的程序纸上,再通过打字机噼噼啪啪地打到纸带上。她听说过从键盘和屏幕输入这回事,现在竟然真-的看到了。但更令她吃惊的是这里的软件技术。她知道了一种叫FORTRAN的东西(注:第一代计算机高级语言),竟能用接近自然语言的代码编写程序。能将数学公-式直接写到代码里,它的编程效率比机器码汇编不知高了多少倍在有限元计算领域,Ansys,Abaqus,Adina和LS-Dyna以及近年开源的Radioss的求解器早期均采用Fortran编写,现在是否依然采用Fortran我们不得而知,但是可以猜测的是,即使在今天,这些商业软件中Fortran依然占据着不小的比重,比如,在Ansys/apdl和LS-DYNA中,有时候计算时的报错提示就是Fortran编译器的报错。实际上,在2024年的今天,高性能计算可选的语言实际上并不多,大部分时候基本上还是Fortran,C/C++。其他可能还有诸如Julia等。在使用Fortran编写数值计算程序时,矩阵乘法是最常见的内容之一,在Fortran中,矩阵乘法有多种方式可以实现,自带的库函数matmul就能直接实现矩阵乘法。在实际中,对于较大的矩阵,通常也并不直接采用matmul,可能会自行编写或者调用MKL库的DGEMM函数完成。本文不讨论调库操作,仅对自行编写矩阵乘法时的效率进行讨论。program main
implicit none
integer,parameter::n=3000
real(8),allocatable::a(:,:),b(:,:),c(:,:)
integer::i,j,k
real(8)::t1,t2
allocate(a(n,n))
allocate(b(n,n))
allocate(c(n,n))
a=1.0
b=2.0
c=0.0
call CPU_TIME(t1)
do i=1,n
do j=1,n
do k=1,n
c(i,j)=c(i,j)+a(i,k)*b(k,j)
enddo
enddo
enddo
call CPU_TIME(t2)
write(*,*)"time",t2-t1
write(*,*)c(n,n)
deallocate(a)
deallocate(b)
deallocate(c)
end program main
3000阶稠密矩阵所花时间为99.78s,还是比较慢的。接下来,注意到在Fortran中,数组以列优先存储,即a(1,1)的后一个元素是a(2,1),因此实际上应尽量使得循环中相邻两次循环取的地址连续,即内存循环变量尽量放在数组的第一个下标。而在上面的程序中,仅b的下标k为最内层循环变量,而a和c的第一下标均为最外层循环变量。因此可以考虑将循环变量交换顺序,得到以下代码:program main
implicit none
integer,parameter::n=3000
real(8),allocatable::a(:,:),b(:,:),c(:,:)
integer::i,j,k
real(8)::t1,t2
allocate(a(n,n))
allocate(b(n,n))
allocate(c(n,n))
a=1.0
b=2.0
c=0.0
call CPU_TIME(t1)
do j=1,n !这里交换了i,j和k的顺序,顺序采用j-k-i
do k=1,n
do i=1,n
c(i,j)=c(i,j)+a(i,k)*b(k,j)
enddo
enddo
enddo
call CPU_TIME(t2)
write(*,*)"time",t2-t1
write(*,*)c(n,n)
deallocate(a)
deallocate(b)
deallocate(c)
end program main
在交换后,依然只有b的第一个下标不是最内层而是次内层,但是a和c的下标是最内层循环而非最外层循环。计算效率如下:果然,计算时间从99.78s减少到了7.875s,速度快了10倍多。果然影响很大!采用交换顺序后,速度从9.7343s降至9.609375s,降幅不大,但是比gfortran的I-J-K顺序仍然快不少,略慢于gfortran的O3优化。二者均很快,甚至I-J-K的速度更快,且速度是gfortran2-3倍。由此可知,当采用Ifort并采用-O3优化时,由于ifort编译器内部优化,在矩阵乘法这一问题上I-J-K的顺序可能并不敏感,总之都很快!当采用gfortran时,采用J-K-I的顺序效率可能是I-J-K的效率的10倍左右,真是令人吃惊!