Python 为什么Fortran中的单变量Horner比NumPy快,而二变量Horner不是

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。不幸的是,我观察到,

我想用Python执行多项式演算。
numpy
中的
多项式
包对我来说不够快。因此,我决定在Fortran中重写几个函数,并使用
f2py
创建易于导入Python的共享库。目前,我正在对照其对应的
numpy
标准测试我的单变量和双变量多项式评估例程

在单变量例程中,我使用as do
numpy.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'