背景
本文旨在为程序加速提供一些代码方面的优化思路,通过优化代码结构、设计高性能计算方案,来有效加速程序运行,提高程序运行效率。需要注意的是,本文只涉及代码层面的加速方案,不包括算法、硬件等方面的优化措施。文章的撰写基于个人经验,如果有不足之处,敬请指出。
代码优化
1 并行化
程序优化的另一种思路是通过并行化加速程序。并行化需要软件与硬件配合,但前提是总任务能够被分解为同时进行的子任务。并行化有两种方式,一种依靠多核CPU实现多进程操作,一种依靠GPU完成。下面我们将分别介绍这两种方式的实现方法。
1.1 CPU多进程操作
1.1.1 多进程
当我们在电脑上运行程序时,实际上是启动了一个进程。进程是指在操作系统中运行的一个程序,它占据着系统的一些资源,如内存、CPU时间等。在传统的单进程计算模型中,所有的任务都在同一个进程中执行,如果任务需要进行复杂的计算,就会耗费大量时间,且无法利用多核CPU的优势。而多进程技术可以将任务分解为多个子任务,每个子任务在一个独立的进程中执行,从而实现并行计算,提高了计算效率。
1.1.2 多进程的Python实现
这是multiprocessing库的代码框架
import multiprocessing
def worker(num:
"""子进程要执行的任务"""
print(f"Worker {num} is running"
return
if __name__ == "__main__":
processes = []
num_processes = 4
# 创建多个子进程
for i in range(num_processes:
p = multiprocessing.Process(target=worker, args=(i,
processes.append(p
p.start(
# 等待所有子进程完成
for p in processes:
p.join(
print("All workers are done"
在这个例子中,我们首先定义了一个worker
函数,它是每个子进程要执行的任务。然后在主进程(if name=="main")中,我们创建了num_processes
个子进程,并将它们添加到processes
列表中。接着,我们遍历processes
列表,用Process类中的start(方法启动每个子进程,并等待它们完成。最后输出"All workers are done",表示所有子进程都已经执行完毕。
1.2 GPU
相比于CPU,GPU有着更多的计算核心和更高的计算能力,可以更好地支持并行化计算。因此,利用GPU进行代码优化可以大幅度提高程序的运行效率。而要在Python中使用GPU进行代码优化,则需要使用GPU编程框架,比较常见的有NVIDIA开发的CUDA框架以及OpenCL框架。
1.2.1 GPU并行计算的Python实现
以下是一个简单的利用CUDA框架实现向量加法的例子:
import numpy as np
from numba import cuda
# 定义向量加法函数
@cuda.jit
def vector_add(a, b, c:
i = cuda.grid(1
if i < len(c:
c[i] = a[i] + b[i]
# 定义主程序
if __name__ == '__main__':
# 定义向量大小
n = 100000
# 在主机上生成随机向量
a = np.random.randn(n.astype(np.float32
b = np.random.randn(n.astype(np.float32
c = np.zeros(n, dtype=np.float32
# 将数据传输到GPU显存中
d_a = cuda.to_device(a
d_b = cuda.to_device(b
d_c = cuda.to_device(c
# 定义线程块和线程数量
threads_per_block = 64
blocks_per_grid = (n + (threads_per_block - 1 // threads_per_block
# 执行向量加法操作
vector_add[blocks_per_grid, threads_per_block](d_a, d_b, d_c
# 将结果从GPU显存中传输回主机内存
d_c.copy_to_host(c
# 打印结果
print(c
在上述代码中,我们首先定义了一个vector_add
函数,用于将两个向量相加并将结果存储在第三个向量中。然后我们生成了两个随机向量,并将其传输到GPU显存中。接着,我们定义了线程块和线程数量,并在GPU部分,我们需要注意的是代码的向量化,也就是多利用矩阵运算编写代码,而不是多重for loop的串行计算。这样可以充分利用GPU的并行计算能力,加速神经网络的训练过程。例如,可以使用cuBLAS库来实现矩阵乘法,使用cuDNN库来实现卷积操作等。执行了向量加法操作。最后,我们将结果从GPU显存中传输回主机内存,并打印出结果。
1.2.2利用GPU训练神经网络
当涉及到训练深度神经网络时,GPU可以发挥其优势,因为神经网络的训练往往需要进行大量的矩阵运算,这正是GPU擅长的任务。为了使用GPU进行深度神经网络的训练,需要利用一些特定的框架和库,比如TensorFlow和PyTorch。
在利用CUDA框架进行神经网络训练时,首先,需要定义一个在GPU上运行的张量(tensor)来存储神经网络的参数。然后,将数据加载到GPU内存中,并将计算任务分配到GPU核心上。
下面是一个简单的基于PyTorch和CUDA的神经网络训练代码示例:
import torch
import torch.nn as nn
import torch.optim as optim
# 定义神经网络模型
class Net(nn.Module:
def __init__(self:
super(Net, self.__init__(
self.conv1 = nn.Conv2d(3, 6, 5
self.pool = nn.MaxPool2d(2, 2
self.conv2 = nn.Conv2d(6, 16, 5
self.fc1 = nn.Linear(16 * 5 * 5, 120
self.fc2 = nn.Linear(120, 84
self.fc3 = nn.Linear(84, 10
def forward(self, x:
x = self.pool(F.relu(self.conv1(x
x = self.pool(F.relu(self.conv2(x
x = x.view(-1, 16 * 5 * 5
x = F.relu(self.fc1(x
x = F.relu(self.fc2(x
x = self.fc3(x
return x
# 定义训练数据和标签
inputs = torch.randn(1, 3, 32, 32
labels = torch.randn(1, 10
# 将神经网络模型移动到GPU上
net = Net(.cuda(
# 将训练数据和标签移动到GPU上
inputs = inputs.cuda(
labels = labels.cuda(
# 定义损失函数和优化器
criterion = nn.MSELoss(
optimizer = optim.SGD(net.parameters(, lr=0.01
# 开始训练
for epoch in range(100:
optimizer.zero_grad(
outputs = net(inputs
loss = criterion(outputs, labels
loss.backward(
optimizer.step(
print('Epoch %d, Loss: %.4f' % (epoch+1, loss.item(
在上面的代码中,我们通过xxx.cuda(的方法将模型、训练数据和标签都加载到GPU内存中,从而使得训练循环中涉及的运算全部在GPU上完成。
1.2.3 注意事项
这是因为,GPU相较于CPU只是强在并行上,但是计算能力是差于CPU的,多层for循环这样的串行计算并不适合GPU。用沐神的话说,如果把CPU比作是一个大学生的话,GPU就像是一群小学生。大学生可以做微积分这样的任务,小学生只能做加减乘除。但如果把一个微积分这样的任务分解成多个加减乘除,那么一群小学生这样的GPU的优势就体现出来了。
如果没有刻意注意代码的向量化,那么很容易想到的思路是这样的:
def lossFunc(y_pred,sols,objs:
batch size=y_pred.shape[0]
loss=torch.tensor(0.0
for i in range(batch_size:
nSols=sols[i].shape[0] #当前batch(MIP下的可行解数目
nVars=sols[i].shape[1]
#目标断数归-化
den=objs[i].sum(
for l in range(objs[i].shape[0]:
objs[i][l]=objs[i][l]/den
den=sum(exp(-objs[i]#计算wii系数的分母
sum1=torch.tensor(0.0
for j in range(nSols:
#计算权重wij
w=exp(-objs[il[j]/den
#计算可行解生成的概率
P=torch.tensor(1.9
for k in range(nVars:
if sols[il[j,k]==1:
P=p*y_pred[i][k]
elif sols[il[j,k]==0:
P=p*(1-y_pred[i][k]
#计算求和
sum1+=w*p
loss+=sum1
loss=-loss
return loss
但实际上用多重for循环设计的损失函数并不利于在GPU上工作,甚至比不上在CPU上执行这样的代码。最后测试结果发现如果使用这样的损失函数,一个epoch大约跑1个小时....所以也不可能把神经网络训练出来了。
def lossFunc(y,sols,objs:
"""
损失函数
Parameters
----------
y : 神经网络输出 batch_size x nVars
sols : 可行解集合 batch_size x nSols x nVars
objs : 可行解对应的目标函数值 batch_size x nSols
Returns
-------
loss : 在当前batch上的损失
"""
objs=objs/15
eObjs=exp(-objs
den=eObjs.sum(axis=1
den=den.unsqueeze(1
w=eObjs/den
y=y.unsqueeze(1
p=y*sols+(1-y*(1-sols
p=log(p+1e-45
P=p.sum(axis=2
loss=-(w*P.sum(
return loss
所以正确的写法应该是这样的,不仅在GPU上加速效果明显,并且看起来简洁多了...但是缺点就是,得不断利用矩阵运算的广播机制,并且最好在小规模的例子上多手算几遍,边算边设计。
2 编译器加速
2.1 原理
编译器优化的方式有很多种,其中最常见的方式包括:
-
循环展开:循环展开是指将循环体中的代码重复执行几次,从而减少循环的次数。这样可以提高程序的运行速度。
为了帮助开发者更加方便地利用编译器优化代码,一些开源JIT编译器如Numba被开发出来。这些编译器可以将Python等解释型语言代码转换为可执行的机器代码,从而提高程序的运行速度。
2.2 基于Numba的Python加速方案
下面是一个使用Numba实现代码加速的大致代码框架:
- 导入numba库
- 定义一个需要优化的函数
- 使用 @numba.jit 装饰器对函数进行装饰,生成numba优化后的函数
- 调用优化后的函数
假设我们有一个多重嵌套的for循环,用于计算一个矩阵的行列式。这是一个非常计算密集的操作,可以使用Numba进行加速。
def det(matrix:
n = len(matrix
if n == 1:
return matrix[0][0]
elif n == 2:
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]
else:
result = 0
for j in range(n:
sub_matrix = []
for i in range(1, n:
row = []
for k in range(n:
if k != j:
row.append(matrix[i][k]
sub_matrix.append(row
result += matrix[0][j] * det(sub_matrix * (-1 ** j
return result
可以看到,这个函数包含了多重嵌套的for循环,非常容易受到Python解释器的性能限制。现在我们使用Numba进行加速。
import numba
@numba.jit(nopython=True
def det(matrix:
n = len(matrix
if n == 1:
return matrix[0][0]
elif n == 2:
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]
else:
result = 0
for j in range(n:
sub_matrix = np.zeros((n-1, n-1
for i in range(1, n:
for k in range(n:
if k != j:
sub_matrix[i-1, k-(k>j] = matrix[i, k]
result += matrix[0][j] * det(sub_matrix * (-1 ** j
return result
我们使用了@numba.jit(nopython=True
装饰器,将函数声明为Numba可加速的函数。同时,我们使用了Numpy数组代替了Python的列表,并使用Numpy数组的切片操作和广播功能,以减少循环和内存分配。
import time
import numpy as np
# Generate a random 10x10 matrix
matrix = np.random.rand(10, 10
# Time the original Python code
start = time.time(
d = det(matrix
end = time.time(
print(f"Python code took {end-start:.4f} seconds, result={d}"
# Time the Numba-optimized code
start = time.time(
d = det(matrix
end = time.time(
print(f"Numba-optimized code took {end-start:.4f} seconds, result={d}"
测试结果:
Python code took 0.5960 seconds, result=-0.004127521725273144
Numba-optimized code took 0.0040 seconds, result=-0.004127521725273144
使用Numba优化Python代码的大致流程如上所述。对于需要优化的函数,我们可以通过添加numba.jit 装饰器实现代码优化,该装饰器会自动进行类型推导并将Python代码编译为机器码。
此外,还需要注意的是,Numba在进行代码优化时,会将Python代码转换为LLVM IR(Intermediate Representation,中间表示形式),这一过程可能会导致代码的可读性降低。因此,在使用Numba进行优化时,需要仔细考虑代码可读性和可维护性的平衡。
总结
值得一提的是,这篇技术博客是在chatGPT的帮助下共同完成的。作者提供了大致的写作思路和要点,而主要内容和其中的代码案例均由chatGPT构建。我们相信,本篇博客对于想要提高代码性能和效率的程序员们会有所帮助。