如何优化Python中计算距离的checkRadars函数以提升性能?
我用Python和Pygame开发了一款测试神经网络(主要针对NEAT算法)的简易赛车游戏,玩家需操控车辆沿赛道行驶且不能触碰边界。为让神经网络“感知”环境,每辆车都配备了多个传感器,用于计算不同角度下到赛道边界(由mask定义)的距离。少量车辆时运行正常,但当赛道上有20辆及以上车辆时,运行速度大幅下降。通过cProfile分析发现,checkRadars()函数是性能瓶颈——每帧更新时每辆车都会调用该函数。我已将部分计算移至循环外进行优化,但性能仍未达标,且length参数无法进一步降低(当前值已偏低),寻求进一步优化该函数的方法。
cProfile分析输出
38382471 function calls in 52.762 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.001 0.001 52.762 52.762 <string>:1(<module>) 26435 0.158 0.000 1.086 0.000 cargame.py:113(draw) 26435 0.045 0.000 0.045 0.000 cargame.py:121(drawRadars) 26435 14.103 0.001 22.550 0.001 cargame.py:128(checkRadars) 26435 0.129 0.000 24.262 0.001 cargame.py:149(update) 26435 0.060 0.000 0.282 0.000 cargame.py:157(checkColliding) 273 0.001 0.000 0.011 0.000 cargame.py:169(drawOnMouse)
待优化的checkRadars函数代码
def checkRadars(self, mask): xcenter, ycenter = self.rect.center #self.radars.clear() v = (180-180*self.blindspot)/(self.radarCount-1) w = (90-90*self.blindspot) # dont mind these weird calculations for n in range(self.radarCount): length = 1 x, y = xcenter, ycenter rangle = v*n-w rad = -math.radians(self.angle+rangle) while isInBound(x, y) and not mask.get_at((x, y)) and length < 100: length += 1 x = int(xcenter + math.sin(rad) * length) y = int(ycenter - math.cos(rad) * length) if length == 100: length = length*10
1. 预计算三角函数值
当前while循环中反复计算math.sin(rad)和math.cos(rad),但每个雷达角度的这两个值是固定的,可提前计算并复用:
def checkRadars(self, mask): xcenter, ycenter = self.rect.center v = (180-180*self.blindspot)/(self.radarCount-1) w = (90-90*self.blindspot) for n in range(self.radarCount): length = 1 rangle = v*n-w rad = -math.radians(self.angle+rangle) # 预计算三角函数值 sin_rad = math.sin(rad) cos_rad = math.cos(rad) x, y = xcenter, ycenter while isInBound(x, y) and not mask.get_at((x, y)) and length < 100: length += 1 x = int(xcenter + sin_rad * length) y = int(ycenter - cos_rad * length) if length == 100: length = length*10
2. 替换mask.get_at()为高效线段碰撞检测
mask.get_at()是单像素级检测,每次调用开销极大。改用Pygame的mask线段碰撞检测,直接获取雷达线段与边界的交点,避免逐像素遍历:
def checkRadars(self, mask): xcenter, ycenter = self.rect.center v = (180-180*self.blindspot)/(self.radarCount-1) w = (90-90*self.blindspot) max_length = 100 for n in range(self.radarCount): rangle = v*n-w rad = -math.radians(self.angle+rangle) # 计算雷达线段的终点坐标 end_x = int(xcenter + math.sin(rad) * max_length) end_y = int(ycenter - math.cos(rad) * max_length) # 创建线段的临时mask并检测碰撞 line_surf = pygame.Surface((abs(end_x-xcenter)+1, abs(end_y-ycenter)+1)) pygame.draw.line(line_surf, (255,255,255), (0,0), (end_x-xcenter, end_y-ycenter)) line_mask = pygame.mask.from_surface(line_surf) # 获取碰撞点偏移量 collision_offset = mask.overlap(line_mask, (xcenter, ycenter)) if collision_offset: # 计算实际距离 length = math.hypot(collision_offset[0], collision_offset[1]) else: length = max_length * 10 # 后续处理length...
注:可预创建常用角度的线段mask,减少重复创建Surface的开销。
3. 增量式坐标计算,减少乘法操作
将当前的xcenter + sin_rad * length改为增量累加,每次循环仅做加法,降低计算开销:
def checkRadars(self, mask): xcenter, ycenter = self.rect.center v = (180-180*self.blindspot)/(self.radarCount-1) w = (90-90*self.blindspot) for n in range(self.radarCount): length = 1 rangle = v*n-w rad = -math.radians(self.angle+rangle) sin_rad = math.sin(rad) cos_rad = math.cos(rad) # 计算每步的坐标增量 dx = sin_rad dy = -cos_rad x, y = xcenter, ycenter while isInBound(x, y) and not mask.get_at((int(x), int(y))) and length < 100: length += 1 x += dx y += dy if length == 100: length = length*10
若isInBound支持浮点坐标,可避免每次转换整数,进一步提升速度。
4. 减少isInBound()调用次数
若赛道mask已包含边界信息(即边界外的区域不在mask内),可直接移除isInBound()判断;若必须保留,可将其移至循环前,先判断雷达终点是否在边界内,再决定是否进入循环,避免每次循环都调用。
5. 批量处理多车辆雷达检测
将所有车辆的中心坐标、角度等数据存入numpy数组,利用向量化计算批量生成所有雷达线段,再一次性与mask做碰撞检测,利用CPU并行能力减少循环开销。
内容的提问来源于stack exchange,提问作者plankins

