在Python中加速计算(磁场下粒子的模拟)

3
我遇到了一些Python程序速度慢的问题。这个程序叫做“磁场中铁磁粒子的模拟”,更具体的是在不具有磁性的液体中进行模拟。程序可以运行,但和用C++编写的同样程序相比速度非常慢。但是我已经将其编写成Python代码,并且它是一个学习项目。
总体来说,程序代码基于循环,有大量的浮点数计算。有随机数量的颗粒(随机生成位置),它们在磁场的作用下互相作用。
这是初始位置: http://i.stack.imgur.com/T15Bb.jpg 这是最终位置: http://i.stack.imgur.com/0nU5D.jpg 主循环(在SymMain.py中,用变量k表示)迭代时间步长,在此期间计算颗粒的坐标以及对其施加的力(引力和微小的斥力)。为了加快速度,我想使用并行处理同时计算多个迭代次数,但这是不可能的,因为一个迭代的计算取决于前一个迭代的计算结果。
我不知道Python的速度比C++慢得这么多。例如,模拟529个颗粒在一个时间步长内的计算(在我的电脑上):
C++约0.5秒
Python约50秒
那么,如何加速程序呢?请帮助我。
这段代码只是计算,不像图片中绘制粒子。模拟颗粒的数量在SymConst.py中,它是nrH * nrL。
SymMain.py
#coding:windows-1250

from os import system
from SymCalc import *
from SymParticle import *


if __name__ == '__main__':
    App = SymCalc()
    App.MainLoop()

SymParticle.py

#coding:windows-1250

from random import randint
from math import *
from SymConst import *
from SymParticle import *

class SymCalc(object):
    def __init__(self):
        # declaration lists containing the properties of the particles
        ParticleList = []
        ParticleListTemp = []
        t = 0.0

        # the initial values ​​of particle
        for x in range(0, nParticle):
            ParticleList.append(Particle(x+1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 8e-6))
            ParticleListTemp.append(Particle(x+1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 8e-6))

        # generating random coordinates X, Y 
        for x in range(0, nParticle):
            self.Rand(ParticleList[x], x)

        # time steps
        for k in range(0, k0):
            print "Time step = {0}".format(k)

            # calculation of forces
            for i in range(0, nParticle):
                for j in range(0, i-1):
                    self.ParticleCalculate(ParticleList[i], ParticleList[j], 1)
                for j in range(i+1, nParticle):
                    self.ParticleCalculate(ParticleList[i], ParticleList[j], 1)

            # display data
            if(k%Constant == 0):
                for i in range(0, nParticle):
                    self.Print(k, i, ParticleList[i], t)

            # changing the position of the particle
            for i in range(0, nParticle):
                self.ChangeOfPosition (ParticleList[i], dt)

                # reset forces
                for j in range(0, nParticle):
                    self.ParticleCalculate(ParticleList[i], ParticleList[j], 0)
                    self.ParticleCalculate(ParticleList[i], ParticleListTemp[j], 0)

            # next time step
            t += dt


    # random coordinates of the particles
    def Rand(self, part, lp):
        l = lp%nrL    # współrzędna X pola      
        part.x = (l-nrL/2)*(L0/nrL) + ((randint(0,32767)%100)-50)*(L0/nrL-2*part.r)/99    # X

        h = (lp+1-(lp)%nrL)/nrL    # Y      
        part.y = (h-nrH/2)*(H0/nrH) + ((randint(0,32767)%100)-50)*(H0/nrH-2*part.r)/99    # współrzędna Y cząstki


    # calculating function of the force acting on the particles
    def ParticleCalculate(self, part, part_tmp, p):
        # auxiliary variables
        # r_4, dx, dy, rr, sum, sx, sy, chi_eff, tmp, frepx, frepy, f0
        md = []

        if(p == 0):
            part.frx = 0
            part.fry = 0
            part.fx = 0
            part.fy = 0

        if(p == 1):
            # versor coordinates connecting the geometrical center of the particle
            dx = part.x - part_tmp.x
            dy = part.y - part_tmp.y

            # the distance between the geometric means of the particle
            rr = sqrt(dx*dx + dy*dy)

            if(rr < 0.85*(part.r + part_tmp.r)):
               print "ERROR: Invalid distance between the particles! Simulation aborted..."

            if(rr >= 10*part.r):
                # magnetic dipoles
                chi_eff = (3.*(MI_P - 1.))/((MI_P - 1) + 3.)

                md.append((4.*MI_0*pi*part.r*part.r*part.r*chi_eff*M_H0)/3.0)
                md.append((4.*MI_0*pi*part_tmp.r*part_tmp.r*part_tmp.r*chi_eff*M_H0)/3.0)

                tmp = pow(rr,7)

                # first member
                sum = (5.*(md[0]*part.nx*dx + md[0]*part.ny*dy) * (md[1]*part_tmp.nx*dx + md[1]*part_tmp.ny*dy)) / tmp
                sx = sum*dx
                sy = sum*dy
                tmp = tmp / (rr*rr)

                # second member
                sum = (md[0]*part.nx*md[1]*part_tmp.nx + md[0]*part.ny*md[1]*part_tmp.ny) / tmp
                sx -= sum*dx
                sy -= sum*dy

                # third member
                sx -= (md[0]*(md[1]*part_tmp.nx*dx + md[1]*part_tmp.ny*dy)*part.nx + md[1]*(md[0]*part.nx*dx + md[0]*part.ny*dy)*part_tmp.nx)/tmp
                sy -= (md[0]*(md[1]*part_tmp.nx*dx + md[1]*part_tmp.ny*dy)*part.ny + md[1]*(md[0]*part.nx*dx + md[0]*part.ny*dy)*part_tmp.ny)/tmp

                # finally
                tmp = (-3./(4*pi*MI_0))
                sx *= tmp
                sy *= tmp
                part.fx += sx
                part.fy += sy

                # short-range repulsive force
                tmp = pow(rr,15)
                r_4 = pow((part.r+part.r),4)

                f0 = 3.*fabs(md[0])*fabs(md[1])/(4.*pi*MI_0*r_4)
                frepx = pow((part.r+part.r),15)*dx/(tmp*rr)*f0
                frepy = pow((part.r+part.r),15)*dy/(tmp*rr)*f0

                part.frx += frepx;
                part.fry += frepy;


    # change the position of the particle
    def ChangeOfPosition(self, part, dt):
        part.ddx = 0
        part.ddy = 0

        part.ddx = 1/(6*pi*part.r*eta)*(part.fx+part.frx)*dt
        part.ddy = 1/(6*pi*part.r*eta)*(part.fy+part.fry)*dt

        # particles new position value
        part.x += part.ddx
        part.y += part.ddy

        if(part.x < -L0/2):
            part.x += L0
        elif(part.x > L0/2):
            part.x -= L0
        elif(part.y < -H0/2):
            part.y += H0
        elif(part.y > H0/2):
            part.y -= H0



    # display data
    def Print(self, k, i, part, t):
        print "-"*50
        print "\nParticle {0}".format(i+1)
        print "The resultant magnetostatic force fx = {0}".format(part.fx - part.frx)
        print "The resultant magnetostatic force fy = {0}\n".format(part.fy - part.fry)
        if(i == nParticle-1):
            print "\n\t\t---t={0}[s]---".format(t)
        print "-"*50

SymParticle.py

#coding:windows-1250

class Particle(object):
    # generating a particle properties
    def __init__(self, num, x, y, fx, fy, frx, fry, nx, ny, ddx, ddy, r):
        self.num = num     
        self.x = x     
        self.y = y     
        self.fx = fx     
        self.fy = fy     
        self.frx = frx     
        self.fry = fry     
        self.nx = nx     
        self.ny = ny     
        self.ddx = ddx     
        self.ddy = ddy     
        self.r = r     

SymConst.py

#coding:windows-1250

### Constant
M_H0 = 3e4
MI_0 = 12.56e-7     
MI_P = 2000
eta = 0.1           
k0 = 10001
dt = 1e-6           # time step      
H0 = 5.95e-4
L0 = 5.95e-4
nrH = 4
nrL = 4
Constant = 100
nParticle = nrH*nrL    # number of particle

4
如果你需要进行大量数值运算,基本上必须使用 numpy - senshin
4
@senshin所言非虚。与C语言等语言相比,Python运行速度极慢,特别是在存在大量嵌套循环或函数调用时。使用numpy编写代码能够获得接近或超越C语言的性能表现,但需要对数值计算的思维方式进行基本调整(你要操作整个数组,而不是单个元素)。学习numpy不是一个5分钟的任务,但一定是值得的。这是官方教程 - loopbackbee
请注意代码风格,例如在此处阅读:http://www.python.org/dev/peps/pep-0008/#function-names请注意,上面链接是关于 Python 函数命名规范的文档。 - Foo Bar User
谢谢回答,我考虑过numpy,但时间不多,你认为使用用C++编写的DLL文件进行计算处理部分怎么样?这有多难? - Kamilos
@Kamilos 如果您已经知道如何创建DLL并在Python中使用它们,那么这可能是一个好主意,但否则我会选择numpy或纯C / C ++。 Python / C接口不像它本应该的那样容易。 您可能还想检查cython和pypy。 Pypy可以为您的代码带来显着的速度提升,而无需进行任何更改。 - loopbackbee
1个回答

5

简短总结 - 使用PyPy!

为了解决这个问题,首先使用Python cProfile 模块对您的代码进行分析,找出所有时间都在哪里使用(请注释print语句):

$ python -m cProfile -s time SymMain.py 
         30264977 function calls in 34.912 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  7370737   27.547    0.000   30.501    0.000 SymCalc.py:62(ParticleCalculate)
        1    3.626    3.626   34.909   34.909 SymCalc.py:8(__init__)
 11101110    1.715    0.000    1.715    0.000 {math.pow}
   160016    0.502    0.000    0.502    0.000 SymCalc.py:128(ChangeOfPosition)
  4440476    0.498    0.000    0.498    0.000 {method 'append' of 'list' objects}
  4440444    0.454    0.000    0.454    0.000 {math.fabs}
  2250226    0.287    0.000    0.287    0.000 {math.sqrt}
   500154    0.280    0.000    0.280    0.000 {range}
        1    0.001    0.001    0.002    0.002 random.py:40(<module>)
        1    0.001    0.001    0.001    0.001 hashlib.py:55(<module>)
        1    0.001    0.001    0.003    0.003 SymCalc.py:2(<module>)
     1616    0.000    0.000    0.000    0.000 SymCalc.py:151(Print)
        1    0.000    0.000   34.912   34.912 SymMain.py:3(<module>)
       32    0.000    0.000    0.000    0.000 SymParticle.py:4(__init__)
---8<--- snip ---8<---

在你的情况下,大部分时间都花在了SymCalc.py:62(ParticleCalculate)函数中。看一看这个函数,它似乎主要是内存访问和计算。这是一个很好的使用PyPy的案例!

PyPy是Python语言(2.7.3和3.2.3)的快速、兼容的替代实现。它有几个优点和不同的特点:

  • 速度:由于其即时编译器,Python程序在PyPy上运行通常更快。(什么是JIT编译器?)
  • 内存使用:大而耗费内存的Python程序可能会占用比在CPython中少的空间。
  • 兼容性:PyPy与现有的Python代码高度兼容。它支持cffi,并且可以运行流行的Python库,如twisted和django。
  • 沙箱环境:PyPy提供了以完全安全的方式运行不受信任的代码的能力。
  • 无栈模式(Stackless):PyPy默认支持无栈模式,提供微线程以进行大量并发。
  • 以及其他功能。
$ time pypy SymMain.py 
real    0m1.071s
user    0m1.025s
sys 0m0.042s

1.071秒 - 好多了!与我的系统上的原始执行时间相比:

$ time python SymMain.py 

real    0m29.766s
user    0m29.646s
sys 0m0.103s

你是否已经在你的Python中安装了numba?如果你将autojit应用于ParticulateCalculate函数,我会很感兴趣看到速度结果。 - Caleb Hattingh
@cjrh,我对numba不是很熟悉,但当我尝试使用它时,出现了错误“NotImplementedError:无法从{i64,i8 *} *转换为{i64,i8 *}。” - Peter Gibson
@cjrh 哎呀!这让NumPy的“零努力”解决方案看起来更好了 :) - Peter Gibson
@Kamilos,我不确定它是否兼容,你最好提出一个新问题询问。 - Peter Gibson
@Peter Gibson,我已经检查过了,Pypy模块无法与wxPython一起使用。因此,我必须使用Pypy单独进行计算,并使用wxPython可视化数据界面。 感谢您的帮助:D - Kamilos
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接