Python 为什么Fortran中的单变量Horner比NumPy快,而二变量Horner不是
我想用Python执行多项式演算。Python 为什么Fortran中的单变量Horner比NumPy快,而二变量Horner不是,python,arrays,numpy,fortran,f2py,Python,Arrays,Numpy,Fortran,F2py,我想用Python执行多项式演算。numpy中的多项式包对我来说不够快。因此,我决定在Fortran中重写几个函数,并使用f2py创建易于导入Python的共享库。目前,我正在对照其对应的numpy标准测试我的单变量和双变量多项式评估例程 在单变量例程中,我使用as donumpy.polynomy.polynomy.polyval。我已经观察到,随着多项式阶数的增加,Fortran例程比对应的numpy更快的因子也会增加 在二元例程中,我使用霍纳方法两次。先在y,然后在x。不幸的是,我观察到,
numpy
中的多项式
包对我来说不够快。因此,我决定在Fortran中重写几个函数,并使用f2py
创建易于导入Python的共享库。目前,我正在对照其对应的numpy
标准测试我的单变量和双变量多项式评估例程
在单变量例程中,我使用as donumpy.polynomy.polynomy.polyval
。我已经观察到,随着多项式阶数的增加,Fortran例程比对应的numpy
更快的因子也会增加
在二元例程中,我使用霍纳方法两次。先在y,然后在x。不幸的是,我观察到,对于增加多项式阶数,对应的numpy
会赶上并最终超过我的Fortran例程。如<代码> NoPy.Posith.Posith.Pulval2D使用类似于我的方法,我认为第二个观察是奇怪的。
我希望这个结果源于我对Fortran和f2py
缺乏经验。有人知道为什么一元例程总是优于二元例程,而二元例程只优于低阶多项式吗
编辑
以下是我最新更新的代码、基准脚本和性能图:
多项式.f95
subroutine polyval(p, x, pval, nx)
implicit none
real(8), dimension(nx), intent(in) :: p
real(8), intent(in) :: x
real(8), intent(out) :: pval
integer, intent(in) :: nx
integer :: i
pval = 0.0d0
do i = nx, 1, -1
pval = pval*x + p(i)
end do
end subroutine polyval
subroutine polyval2(p, x, y, pval, nx, ny)
implicit none
real(8), dimension(nx, ny), intent(in) :: p
real(8), intent(in) :: x, y
real(8), intent(out) :: pval
integer, intent(in) :: nx, ny
real(8) :: tmp
integer :: i, j
pval = 0.0d0
do j = ny, 1, -1
tmp = 0.0d0
do i = nx, 1, -1
tmp = tmp*x + p(i, j)
end do
pval = pval*y + tmp
end do
end subroutine polyval2
subroutine polyval3(p, x, y, z, pval, nx, ny, nz)
implicit none
real(8), dimension(nx, ny, nz), intent(in) :: p
real(8), intent(in) :: x, y, z
real(8), intent(out) :: pval
integer, intent(in) :: nx, ny, nz
real(8) :: tmp, tmp2
integer :: i, j, k
pval = 0.0d0
do k = nz, 1, -1
tmp2 = 0.0d0
do j = ny, 1, -1
tmp = 0.0d0
do i = nx, 1, -1
tmp = tmp*x + p(i, j, k)
end do
tmp2 = tmp2*y + tmp
end do
pval = pval*z + tmp2
end do
end subroutine polyval3
benchmark.py(使用此脚本生成绘图)
结果
编辑对steabert提案的更正
subroutine polyval(p, x, pval, nx)
implicit none
real*8, dimension(nx), intent(in) :: p
real*8, intent(in) :: x
real*8, intent(out) :: pval
integer, intent(in) :: nx
integer, parameter :: simd = 8
real*8 :: tmp(simd), xpower(simd), maxpower
integer :: i, j, k
xpower(1) = x
do i = 2, simd
xpower(i) = xpower(i-1)*x
end do
maxpower = xpower(simd)
tmp = 0.0d0
do i = nx+1, simd+2, -simd
do j = 1, simd
tmp(j) = tmp(j)*maxpower + p(i-j)*xpower(simd-j+1)
end do
end do
k = mod(nx-1, simd)
if (k == 0) then
pval = sum(tmp) + p(1)
else
pval = sum(tmp) + p(k+1)
do i = k, 1, -1
pval = pval*x + p(i)
end do
end if
end subroutine polyval
编辑测试代码,以验证正上方的代码对x>1的结果是否不佳
import polynomial as P
import numpy.polynomial.polynomial as PP
import numpy as np
for n in xrange(2,100):
poly1n = np.random.rand(n)
poly1f = np.asfortranarray(poly1n)
x = 2
print np.linalg.norm(P.polyval(poly1f, x) - PP.polyval(x, poly1n)), '\n'
我猜,您的tmp阵列太大了,以至于它需要L2、L3甚至主内存访问,而不是缓存。最好将这些循环分解,一次只处理其中的一大块(条带挖掘)。在双变量情况下,
p
是一个二维数组。这意味着数组的C与fortran顺序是不同的。默认情况下,numpy函数提供C排序,显然fortran例程使用fortran排序
f2py足够聪明,可以处理这个问题,并自动在C和fortran格式数组之间进行转换。但是,这会导致一些开销,这可能是性能降低的原因之一。您可以通过在计时例程之外使用numpy.asfortranarray
手动将p
转换为fortran类型来检查这是否是原因。当然,为了使其有意义,在您的实际用例中,您需要确保您的输入数组符合fortran顺序
f2py有一个选项-DF2PY\u REPORT\u ON\u ARRAY\u COPY
,可以在复制阵列时向您发出警告
如果这不是原因,那么你需要考虑更深入的细节,比如你正在使用的FORTRAN编译器,以及它应用的是什么样的优化。可能会减慢速度的例子包括在堆上而不是堆栈上分配数组(使用对
malloc
的昂贵调用),尽管我预计这样的影响对于较大的数组来说会变得不那么重要
最后,你应该考虑二元拟合的可能性,对于大<代码> n>代码>,麻木例程已经基本上处于最佳效率。在这种情况下,numpy例程可能会花费大部分时间运行经过优化的C例程,相比之下,python代码的开销可以忽略不计。在这种情况下,您不会期望fortran代码显示任何显著的加速。
您的函数非常短,因此通过内联polyval可以获得更好的结果。此外,您还可以通过简单地反转循环来避免指数的计算:subroutine polyval2(p, x, y, pval, nx, ny)
implicit none
real(8), dimension(nx, ny), intent(in), target :: p
real(8), intent(in) :: x, y
real(8), intent(out) :: pval
integer, intent(in) :: nx, ny
real(8) :: tmp
integer :: i, ii
pval = 1.d0
do i = ny, 1
tmp = 1.d0
do ii = nx, 1
tmp = tmp*x + p(ii,i)
end do
pval = pval*y + tmp
end do
end subroutine polyval2
使用这段代码,与您发布的原始代码相比,大型数组的执行时间缩短了约10%。(我用代码Nx=Ny=1000测试了一个纯Fortran程序,gfortran-O3-funroll循环
)
我同意haraldkl的观点,当维度变得太大时,性能的急剧下降对于缓存/内存访问模式来说是非常典型的。露天采矿有帮助,但我不鼓励你自己这么做。改用编译器标志:-floop strip mine
用于gfortran
和(包含在)-O3
用于ifort
。另外,尝试循环展开:-funroll循环
用于gfortran
和ifort
您可以使用
f2py-c--f90flags=“…”
指定这些标志>P>>其他建议,使用<代码> P= NP。ASFRONTROLART(P)在测试之前,定时器确实使性能与NUMPY一致。我将双变量工作台的范围扩展到n_bi=np.array([2**I代表xrange(1,15)])
,这样p矩阵将大于我的三级缓存大小
为了进一步优化这一点,我认为自动编译器选项不会有多大帮助,因为内部循环具有依赖性。仅当手动展开它时,ifort
才会对最里面的循环进行矢量化。使用gfortran
,需要-O3
和-ffast math
。对于受主内存带宽限制的矩阵大小,这会将性能优势从numpy的1倍提高到3倍
更新:将其应用于单变量代码并使用f2py--opt='-O3-ffast math'-c-m多项式.f90
编译后,我得到了benchmark.py的以下源代码和结果:
subroutine polyval(p, x, pval, nx)
implicit none
real*8, dimension(nx), intent(in) :: p
real*8, intent(in) :: x
real*8, intent(out) :: pval
integer, intent(in) :: nx
integer, parameter :: simd = 8
real*8 :: tmp(simd), vecx(simd), xfactor
integer :: i, j, k
! precompute factors
do i = 1, simd
vecx(i)=x**(i-1)
end do
xfactor = x**simd
tmp = 0.0d0
do i = 1, nx, simd
do k = 1, simd
tmp(k) = tmp(k)*xfactor + p(nx-(i+k-1)+1)*vecx(simd-k+1)
end do
end do
pval = sum(tmp)
end subroutine polyval
subroutine polyval2(p, x, y, pval, nx, ny)
implicit none
real*8, dimension(nx, ny), intent(in) :: p
real*8, intent(in) :: x, y
real*8, intent(out) :: pval
integer, intent(in) :: nx, ny
integer, parameter :: simd = 8
real*8 :: tmp(simd), vecx(simd), xfactor
integer :: i, j, k
! precompute factors
do i = 1, simd
vecx(i)=x**(i-1)
end do
xfactor = x**simd
! horner
pval=0.0d0
do i = 1, ny
tmp = 0.0d0
do j = 1, nx, simd
! inner vectorizable loop
do k = 1, simd
tmp(k) = tmp(k)*xfactor + p(nx-(j+k-1)+1,ny-i+1)*vecx(simd-k+1)
end do
end do
pval = pval*y + sum(tmp)
end do
end subroutine polyval2
更新2:如前所述,此代码不正确,至少当大小不能被simd
整除时是如此。它只是展示了手动帮助编译器的概念,所以不要像这样使用它。如果大小不是2的幂,则必须创建一个小的余数循环
subroutine polyval(p, x, pval, nx)
implicit none
real*8, dimension(nx), intent(in) :: p
real*8, intent(in) :: x
real*8, intent(out) :: pval
integer, intent(in) :: nx
integer, parameter :: simd = 8
real*8 :: tmp(simd), vecx(simd), xfactor
integer :: i, j, k
! precompute factors
do i = 1, simd
vecx(i)=x**(i-1)
end do
xfactor = x**simd
tmp = 0.0d0
do i = 1, nx, simd
do k = 1, simd
tmp(k) = tmp(k)*xfactor + p(nx-(i+k-1)+1)*vecx(simd-k+1)
end do
end do
pval = sum(tmp)
end subroutine polyval
subroutine polyval2(p, x, y, pval, nx, ny)
implicit none
real*8, dimension(nx, ny), intent(in) :: p
real*8, intent(in) :: x, y
real*8, intent(out) :: pval
integer, intent(in) :: nx, ny
integer, parameter :: simd = 8
real*8 :: tmp(simd), vecx(simd), xfactor
integer :: i, j, k
! precompute factors
do i = 1, simd
vecx(i)=x**(i-1)
end do
xfactor = x**simd
! horner
pval=0.0d0
do i = 1, ny
tmp = 0.0d0
do j = 1, nx, simd
! inner vectorizable loop
do k = 1, simd
tmp(k) = tmp(k)*xfactor + p(nx-(j+k-1)+1,ny-i+1)*vecx(simd-k+1)
end do
end do
pval = pval*y + sum(tmp)
end do
end subroutine polyval2
subroutine polyval(p, x, pval, nx)
implicit none
real*8, dimension(nx), intent(in) :: p
real*8, intent(in) :: x
real*8, intent(out) :: pval
integer, intent(in) :: nx
integer, parameter :: simd = 4
real*8 :: tmp(simd), vecx(simd), xfactor
integer :: i, j, k, nr
! precompute factors
do i = 1, simd
vecx(i)=x**(i-1)
end do
xfactor = x**simd
! check remainder
nr = mod(nx, simd)
! horner
tmp = 0.0d0
do i = 1, nx-nr, simd
do k = 1, simd
tmp(k) = tmp(k)*xfactor + p(nx-(i+k-1)+1)*vecx(simd-k+1)
end do
end do
pval = sum(tmp)
! do remainder
pval = pval * x**nr
do i = 1, nr
pval = pval + p(i) * vecx(i)
end do
end subroutine polyval
import polynomial as P
import numpy.polynomial.polynomial as PP
import numpy as np
for n in xrange(2,100):
poly1n = np.random.rand(n)
poly1f = np.asfortranarray(poly1n)
x = 2
print "%18.14e" % P.polyval(poly1f, x)
print "%18.14e" % PP.polyval(x, poly1n)
print (P.polyval(poly1f, x) - PP.polyval(x, poly1n))/PP.polyval(x,poly1n), '\n'