Performance 回路优化
我正试图了解在源代码中可以做哪些缓存或其他优化来加快循环。我认为它对缓存非常友好,但是,有没有专家可以在调优这段代码时挤出更多的性能呢Performance 回路优化,performance,caching,optimization,fortran,hpc,Performance,Caching,Optimization,Fortran,Hpc,我正试图了解在源代码中可以做哪些缓存或其他优化来加快循环。我认为它对缓存非常友好,但是,有没有专家可以在调优这段代码时挤出更多的性能呢 DO K = 1, NZ DO J = 1, NY DO I = 1, NX SIDEBACK = STEN(I-1,J-1,K-1) + STEN(I-1,J,K-1) + STEN(I-1,J+1,K-1) + & STEN(I ,J-1,K-
DO K = 1, NZ
DO J = 1, NY
DO I = 1, NX
SIDEBACK = STEN(I-1,J-1,K-1) + STEN(I-1,J,K-1) + STEN(I-1,J+1,K-1) + &
STEN(I ,J-1,K-1) + STEN(I ,J,K-1) + STEN(I ,J+1,K-1) + &
STEN(I+1,J-1,K-1) + STEN(I+1,J,K-1) + STEN(I+1,J+1,K-1)
SIDEOWN = STEN(I-1,J-1,K) + STEN(I-1,J,K) + STEN(I-1,J+1,K) + &
STEN(I ,J-1,K) + STEN(I ,J,K) + STEN(I ,J+1,K) + &
STEN(I+1,J-1,K) + STEN(I+1,J,K) + STEN(I+1,J+1,K)
SIDEFRONT = STEN(I-1,J-1,K+1) + STEN(I-1,J,K+1) + STEN(I-1,J+1,K+1) + &
STEN(I ,J-1,K+1) + STEN(I ,J,K+1) + STEN(I ,J+1,K+1) + &
STEN(I+1,J-1,K+1) + STEN(I+1,J,K+1) + STEN(I+1,J+1,K+1)
RES(I,J,K) = ( SIDEBACK + SIDEOWN + SIDEFRONT ) / 27.0
END DO
END DO
END DO
好的,我想我已经尽了我所能,不幸的是我的结论是没有太多的优化空间,除非你愿意进入并行化。让我们看看为什么,让我们看看你能做什么和不能做什么 编译器优化 如今的编译器非常擅长优化代码,远远超过人类。依靠编译器所做的优化还有一个额外的好处,即它们不会破坏源代码的可读性。无论您做什么,(在优化速度时)总是尝试使用编译器标志的每一个合理组合。您甚至可以尝试多个编译器。就我个人而言,我只使用了gfortran(包含在GCC中)(操作系统是64位Windows),我相信它具有高效和正确的优化技术
-O2
几乎总能大幅提高速度,但即使是-O3
也是安全的赌注(其中包括美味的循环展开)。对于这个问题,我还尝试了-ffast math
和-feexpensive优化
,它们没有任何可测量的效果,但是-march-corei7
(特定于核心i7的cpu体系结构调整),所以我使用-O3-march-corei7
那么它到底有多快呢?
我编写了以下代码来测试您的解决方案,并使用-O3-march-corei7
对其进行了编译。通常在0.78-0.82秒之间
program benchmark
implicit none
real :: start, finish
integer :: I, J, K
real :: SIDEBACK, SIDEOWN, SIDEFRONT
integer, parameter :: NX = 600
integer, parameter :: NY = 600
integer, parameter :: NZ = 600
real, dimension (0 : NX + 2, 0 : NY + 2, 0 : NZ + 2) :: STEN
real, dimension (0 : NX + 2, 0 : NY + 2, 0 : NZ + 2) :: RES
call random_number(STEN)
call cpu_time(start)
DO K = 1, NZ
DO J = 1, NY
DO I = 1, NX
SIDEBACK = STEN(I-1,J-1,K-1) + STEN(I-1,J,K-1) + STEN(I-1,J+1,K-1) + &
STEN(I ,J-1,K-1) + STEN(I ,J,K-1) + STEN(I ,J+1,K-1) + &
STEN(I+1,J-1,K-1) + STEN(I+1,J,K-1) + STEN(I+1,J+1,K-1)
SIDEOWN = STEN(I-1,J-1,K) + STEN(I-1,J,K) + STEN(I-1,J+1,K) + &
STEN(I ,J-1,K) + STEN(I ,J,K) + STEN(I ,J+1,K) + &
STEN(I+1,J-1,K) + STEN(I+1,J,K) + STEN(I+1,J+1,K)
SIDEFRONT = STEN(I-1,J-1,K+1) + STEN(I-1,J,K+1) + STEN(I-1,J+1,K+1) + &
STEN(I ,J-1,K+1) + STEN(I ,J,K+1) + STEN(I ,J+1,K+1) + &
STEN(I+1,J-1,K+1) + STEN(I+1,J,K+1) + STEN(I+1,J+1,K+1)
RES(I,J,K) = ( SIDEBACK + SIDEOWN + SIDEFRONT ) / 27.0
END DO
END DO
END DO
call cpu_time(finish)
!Use the calculated value, so the compiler doesn't optimize away everything.
!Print the original value as well, because one can never be too paranoid.
print *, STEN(1,1,1), RES(1,1,1)
print '(f6.3," seconds.")',finish-start
end program
好的,这就是编译器所能给我们的。下一步是什么
存储中间结果?
正如你可能从问号中怀疑的那样,这一个并没有真正起作用。很抱歉但我们不要急着这么做。
如评论中所述,您当前的代码多次计算每个部分和,这意味着一次迭代的STEN(I+1,J-1,K-1)+STEN(I+1,J,K-1)+STEN(I+1,J+1,K-1)
将是下一次迭代的STEN(I,J-1,K-1)+STEN(I,J,K-1)
,因此无需再次获取和计算,您可以存储这些部分结果。
问题是,我们不能存储太多的部分结果。正如您所说,您的代码已经对缓存非常友好,存储的每个部分和意味着可以在一级缓存中少存储一个数组元素。我们可以从I
的最后几次迭代中存储几个值(索引I-2
、I-3
等的值),但编译器几乎肯定已经这样做了。我有两个证据证明这一怀疑。首先,我的手动循环展开使程序变慢了,大约5%
DO K = 1, NZ
DO J = 1, NY
DO I = 1, NX, 8
SIDEBACK(0) = STEN(I-1,J-1,K-1) + STEN(I-1,J,K-1) + STEN(I-1,J+1,K-1)
SIDEBACK(1) = STEN(I ,J-1,K-1) + STEN(I ,J,K-1) + STEN(I ,J+1,K-1)
SIDEBACK(2) = STEN(I+1,J-1,K-1) + STEN(I+1,J,K-1) + STEN(I+1,J+1,K-1)
SIDEBACK(3) = STEN(I+2,J-1,K-1) + STEN(I+2,J,K-1) + STEN(I+2,J+1,K-1)
SIDEBACK(4) = STEN(I+3,J-1,K-1) + STEN(I+3,J,K-1) + STEN(I+3,J+1,K-1)
SIDEBACK(5) = STEN(I+4,J-1,K-1) + STEN(I+4,J,K-1) + STEN(I+4,J+1,K-1)
SIDEBACK(6) = STEN(I+5,J-1,K-1) + STEN(I+5,J,K-1) + STEN(I+5,J+1,K-1)
SIDEBACK(7) = STEN(I+6,J-1,K-1) + STEN(I+6,J,K-1) + STEN(I+6,J+1,K-1)
SIDEBACK(8) = STEN(I+7,J-1,K-1) + STEN(I+7,J,K-1) + STEN(I+7,J+1,K-1)
SIDEBACK(9) = STEN(I+8,J-1,K-1) + STEN(I+8,J,K-1) + STEN(I+8,J+1,K-1)
SIDEOWN(0) = STEN(I-1,J-1,K) + STEN(I-1,J,K) + STEN(I-1,J+1,K)
SIDEOWN(1) = STEN(I ,J-1,K) + STEN(I ,J,K) + STEN(I ,J+1,K)
SIDEOWN(2) = STEN(I+1,J-1,K) + STEN(I+1,J,K) + STEN(I+1,J+1,K)
SIDEOWN(3) = STEN(I+2,J-1,K) + STEN(I+2,J,K) + STEN(I+2,J+1,K)
SIDEOWN(4) = STEN(I+3,J-1,K) + STEN(I+3,J,K) + STEN(I+3,J+1,K)
SIDEOWN(5) = STEN(I+4,J-1,K) + STEN(I+4,J,K) + STEN(I+4,J+1,K)
SIDEOWN(6) = STEN(I+5,J-1,K) + STEN(I+5,J,K) + STEN(I+5,J+1,K)
SIDEOWN(7) = STEN(I+6,J-1,K) + STEN(I+6,J,K) + STEN(I+6,J+1,K)
SIDEOWN(8) = STEN(I+7,J-1,K) + STEN(I+7,J,K) + STEN(I+7,J+1,K)
SIDEOWN(9) = STEN(I+8,J-1,K) + STEN(I+8,J,K) + STEN(I+8,J+1,K)
SIDEFRONT(0) = STEN(I-1,J-1,K+1) + STEN(I-1,J,K+1) + STEN(I-1,J+1,K+1)
SIDEFRONT(1) = STEN(I ,J-1,K+1) + STEN(I ,J,K+1) + STEN(I ,J+1,K+1)
SIDEFRONT(2) = STEN(I+1,J-1,K+1) + STEN(I+1,J,K+1) + STEN(I+1,J+1,K+1)
SIDEFRONT(3) = STEN(I+2,J-1,K+1) + STEN(I+2,J,K+1) + STEN(I+2,J+1,K+1)
SIDEFRONT(4) = STEN(I+3,J-1,K+1) + STEN(I+3,J,K+1) + STEN(I+3,J+1,K+1)
SIDEFRONT(5) = STEN(I+4,J-1,K+1) + STEN(I+4,J,K+1) + STEN(I+4,J+1,K+1)
SIDEFRONT(6) = STEN(I+5,J-1,K+1) + STEN(I+5,J,K+1) + STEN(I+5,J+1,K+1)
SIDEFRONT(7) = STEN(I+6,J-1,K+1) + STEN(I+6,J,K+1) + STEN(I+6,J+1,K+1)
SIDEFRONT(8) = STEN(I+7,J-1,K+1) + STEN(I+7,J,K+1) + STEN(I+7,J+1,K+1)
SIDEFRONT(9) = STEN(I+8,J-1,K+1) + STEN(I+8,J,K+1) + STEN(I+8,J+1,K+1)
RES(I ,J,K) = ( SIDEBACK(0) + SIDEOWN(0) + SIDEFRONT(0) + &
SIDEBACK(1) + SIDEOWN(1) + SIDEFRONT(1) + &
SIDEBACK(2) + SIDEOWN(2) + SIDEFRONT(2) ) / 27.0
RES(I + 1,J,K) = ( SIDEBACK(1) + SIDEOWN(1) + SIDEFRONT(1) + &
SIDEBACK(2) + SIDEOWN(2) + SIDEFRONT(2) + &
SIDEBACK(3) + SIDEOWN(3) + SIDEFRONT(3) ) / 27.0
RES(I + 2,J,K) = ( SIDEBACK(2) + SIDEOWN(2) + SIDEFRONT(2) + &
SIDEBACK(3) + SIDEOWN(3) + SIDEFRONT(3) + &
SIDEBACK(4) + SIDEOWN(4) + SIDEFRONT(4) ) / 27.0
RES(I + 3,J,K) = ( SIDEBACK(3) + SIDEOWN(3) + SIDEFRONT(3) + &
SIDEBACK(4) + SIDEOWN(4) + SIDEFRONT(4) + &
SIDEBACK(5) + SIDEOWN(5) + SIDEFRONT(5) ) / 27.0
RES(I + 4,J,K) = ( SIDEBACK(4) + SIDEOWN(4) + SIDEFRONT(4) + &
SIDEBACK(5) + SIDEOWN(5) + SIDEFRONT(5) + &
SIDEBACK(6) + SIDEOWN(6) + SIDEFRONT(6) ) / 27.0
RES(I + 5,J,K) = ( SIDEBACK(5) + SIDEOWN(5) + SIDEFRONT(5) + &
SIDEBACK(6) + SIDEOWN(6) + SIDEFRONT(6) + &
SIDEBACK(7) + SIDEOWN(7) + SIDEFRONT(7) ) / 27.0
RES(I + 6,J,K) = ( SIDEBACK(6) + SIDEOWN(6) + SIDEFRONT(6) + &
SIDEBACK(7) + SIDEOWN(7) + SIDEFRONT(7) + &
SIDEBACK(8) + SIDEOWN(8) + SIDEFRONT(8) ) / 27.0
RES(I + 7,J,K) = ( SIDEBACK(7) + SIDEOWN(7) + SIDEFRONT(7) + &
SIDEBACK(8) + SIDEOWN(8) + SIDEFRONT(8) + &
SIDEBACK(9) + SIDEOWN(9) + SIDEFRONT(9) ) / 27.0
END DO
END DO
END DO
更糟糕的是,很容易证明我们已经非常接近理论上可能的最小执行时间。为了计算所有这些平均值,我们需要做的绝对最小值是至少访问每个元素一次,然后将它们除以27.0。因此,您永远不会比下面的代码更快,它在我的机器上的执行时间不到0.48-0.5秒
program benchmark
implicit none
real :: start, finish
integer :: I, J, K
integer, parameter :: NX = 600
integer, parameter :: NY = 600
integer, parameter :: NZ = 600
real, dimension (0 : NX + 2, 0 : NY + 2, 0 : NZ + 2) :: STEN
real, dimension (0 : NX + 2, 0 : NY + 2, 0 : NZ + 2) :: RES
call random_number(STEN)
call cpu_time(start)
DO K = 1, NZ
DO J = 1, NY
DO I = 1, NX
!This of course does not do what you want to do,
!this is just an example of a speed limit we can never surpass.
RES(I, J, K) = STEN(I, J, K) / 27.0
END DO
END DO
END DO
call cpu_time(finish)
!Use the calculated value, so the compiler doesn't optimize away everything.
print *, STEN(1,1,1), RES(1,1,1)
print '(f6.3," seconds.")',finish-start
end program
但是,即使是消极的结果也是结果。如果仅仅访问每个元素一次(除以27.0)就占用了一半以上的执行时间,那就意味着内存访问是瓶颈。然后,也许您可以优化
更少的数据
如果不需要64位双精度的完整精度,可以使用real(kind=4)
类型声明数组。但也许你的real已经是4字节了。在这种情况下,我相信一些Fortran实现支持非标准的16位双精度,或者根据您的数据,您可以只使用整数(可能是浮点乘以一个数字,然后四舍五入为整数)。基类型越小,可以放入缓存的元素就越多。最理想的是integer(kind=1)
,当然,与real(kind=4)
相比,它在我的机器上的速度提高了2倍多。但这取决于你需要的精度
更好的位置
当需要来自相邻列的数据时,列主数组的速度较慢,而行主数组对于相邻行的速度较慢。
幸运的是,有一种称为a的时髦的数据存储方式,它在计算机图形学中确实有。
我不能保证这会有帮助,也许会适得其反,但也许不会。对不起,老实说,我不想自己实现它
并行化
说到计算机图形学,这个问题非常简单,甚至可以在GPU上进行很好的并行处理,但是如果你不想走那么远,你可以使用一个普通的多核CPU。似乎是一个搜索Fortran并行化库的好地方。请确认,这是Fortran,而STEN是一个列主数组?@szmate1618更正我认为您可以做得更多。当前代码多次计算每个部分和。我的意思是一次迭代的
STEN(I+1,J-1,K-1)+STEN(I+1,J,K-1)+STEN(I+1,J+1,K-1)
将是下一次迭代的STEN(I,J-1,K-1)+STEN(I,J,K-1)
,因此无需再次获取和计算,您可以存储这些部分结果。我已经在做一个完整的实现,很快就会发布。等等,我很笨。如果Fortran编译器能够展开循环,那么它可能能够执行与我尝试执行的相同的优化。如果将STEN作为输入参数传递,它可以很容易地做到这一点。Fortran以没有别名、极端循环展开而闻名。这些都不是指针,对吗?real(kind=4)
不必是4字节,它可能根本不存在,事实上在某些编译器中它并不存在。类似地,real(kind=1)
确实存在于某些编译器中,但它可能是一个常见的默认实数。展开可能无效,因为您没有使用parens或其他方法来确保编译器识别相邻赋值之间的公共子表达式。