简介
这篇文章是这系列文章的第二篇,我们的关注点在使用Numpy API为Python编写C扩展模块的过程。在第一部分中,我们建立了一个简单的N体模拟,并发现其瓶颈是计算体之间的相互作用力,这是一个复杂度为O(N^2)的操作。通过在C语言中实现一个时间演化函数,我们大概能以大约70倍来加速计算。
如果你还没有看过第一篇文章,你应该在继续看这篇文章之前先看一下。
在这篇文章中,我们将牺牲我们代码中的一些通用性来提升性能。
回顾
Wrold
是存储N体状态的一个类。我们的模拟将演化一系列时间步长下的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
class World(object): """World is a structure that holds the state of N bodies and additional variables. threads : (int) The number of threads to use for multithreaded implementations. dt : (float) The time-step. STATE OF THE WORLD: N : (int) The number of bodies in the simulation. m : (1D ndarray) The mass of each body. r : (2D ndarray) The position of each body. v : (2D ndarray) The velocity of each body. F : (2D ndarray) The force on each body. TEMPORARY VARIABLES: Ft : (3D ndarray) A 2D force array for each thread's local storage. s : (2D ndarray) The vectors from one body to all others. s3 : (1D ndarray) The norm of each s vector. NOTE: Ft is used by parallel algorithms for thread-local storage. s and s3 are only used by the Python implementation. """ def __init__(self, N, threads=1, m_min=1, m_max=30.0, r_max=50.0, v_max=4.0, dt=1e-3): self.threads = threads self.N = N self.m = np.random.uniform(m_min, m_max, N) self.r = np.random.uniform(-r_max, r_max, (N, 2)) self.v = np.random.uniform(-v_max, v_max, (N, 2)) self.F = np.zeros_like(self.r) self.Ft = np.zeros((threads, N, 2)) self.s = np.zeros_like(self.r) self.s3 = np.zeros_like(self.m) self.dt = dt |
在开始模拟时,N体被随机分配质量m,位置r和速度v。对于每个时间步长,接下来的计算有:
1. 合力F,每个体上的合力根据所有其他体的计算。
2. 速度v,由于力的作用每个体的速度被改变。
3. 位置R,由于速度每个体的位置被改变。
访问宏
我们在第一部分创建的扩展模块使用宏来获取C语言中NumPy数组的元素。下面是这些宏中的一些宏的形式:
1 2 3 |
#define r(x0, x1) (*(npy_float64*)((PyArray_DATA(py_r) + \ (x0) * PyArray_STRIDES(py_r)[0] + \ (x1) * PyArray_STRIDES(py_r)[1]))) |
像这样使用宏,我们能使用像r(i, j)这样的简单标记来访问py_r数组中的元素。不管数组已经以某种形式被重塑或切片,索引值将匹配你在Python中看到的形式。
对于通用的代码,这就是我们想要的。在我们模拟的情况下,我们知道我们的数组的特点:它们是连续的,并且从未被切片或重塑。我们可以利用这一点来简化和提升我们代码的性能。
简单的C扩展 2
在本节中,我们将看到一个修改版本的C扩展,它摈弃了访问宏和NumPy数组底层数据的直接操作。本节中的代码src/simple2.c可在github上获得。
为了进行比较,之前的实现也可在这里获得。
演化函数
从文件的底部开始,我们可以看到,evolve函数与之前的版本一样,以相同的方式解析Python参数,但现在我们利用PyArray_DATA宏来获得一个纸箱底层的内存。我们将这个指针命名为npy_float64
,作为double的一个别名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
简介 这篇文章是这系列文章的第二篇,我们的关注点在使用Numpy API为Python编写C扩展模块的过程。在第一部分中,我们建立了一个简单的N体模拟,并发现其瓶颈是计算体之间的相互作用力,这是一个复杂度为O(N^2)的操作。通过在C语言中实现一个时间演化函数,我们大概能以大约70倍来加速计算。 如果你还没有看过第一篇文章,你应该在继续看这篇文章之前先看一下。 在这篇文章中,我们将牺牲我们代码中的一些通用性来提升性能。 回顾
在开始模拟时,N体被随机分配质量m,位置r和速度v。对于每个时间步长,接下来的计算有: 1. 合力F,每个体上的合力根据所有其他体的计算。 访问宏我们在第一部分创建的扩展模块使用宏来获取C语言中NumPy数组的元素。下面是这些宏中的一些宏的形式:
像这样使用宏,我们能使用像r(i, j)这样的简单标记来访问py_r数组中的元素。不管数组已经以某种形式被重塑或切片,索引值将匹配你在Python中看到的形式。 对于通用的代码,这就是我们想要的。在我们模拟的情况下,我们知道我们的数组的特点:它们是连续的,并且从未被切片或重塑。我们可以利用这一点来简化和提升我们代码的性能。 简单的C扩展 2 在本节中,我们将看到一个修改版本的C扩展,它摈弃了访问宏和NumPy数组底层数据的直接操作。本节中的代码src/simple2.c可在github上获得。 为了进行比较,之前的实现也可在这里获得。 演化函数 从文件的底部开始,我们可以看到,evolve函数与之前的版本一样,以相同的方式解析Python参数,但现在我们利用PyArray_DATA宏来获得一个纸箱底层的内存。我们将这个指针命名为
|