使用Numba加速卷积代码反而更慢,求技术指导
问题
实现了单通道图像的卷积函数,对比使用Numba @njit装饰器和不使用的运行效率,发现带Numba的代码耗时0.6485073566436768秒,不带的仅0.007578611373901367秒,前者速度远慢于后者。尝试添加parallel=True、cache=True参数优化后速度反而更慢,求问题原因及解决办法。
原因分析
- 首次编译开销被计入测试时间:Numba的
@njit装饰器在函数第一次调用时会执行JIT编译,这个编译过程的耗时远大于小图像卷积计算的实际耗时,而单次测试直接把编译时间算进了总耗时。 - 循环结构效率低下:代码中用
xRows、yCols列表存储核坐标再间接遍历的方式,限制了Numba的优化空间,反而不如Python结合Numpy的底层C实现数组操作高效。 - 误用并行加速:当前测试的输出尺寸仅32x32,计算量极小,开启
prange并行带来的线程创建、调度开销远大于并行收益,反而拖慢速度。 - 小计算量场景下Numba优势不显著:32x32的小图像计算量有限,Python+Numpy的向量操作已经足够快,Numba的优化效果在大尺寸图像、批量处理等计算量更大的场景才会显现。
解决办法
- 排除编译时间干扰:测试前先调用一次函数完成编译(预热),再计时实际运行时间;或多次运行取平均,抵消编译开销。
- 优化循环结构:直接遍历核的坐标,避免用额外列表存储,让Numba更好地优化内存访问和循环逻辑。
- 移除不必要的并行:小计算量场景下改用普通
range,避免prange带来的额外开销。 - 利用Numba数组操作优化:尽量使用Numba支持的数组原生操作,减少Python风格的嵌套循环。
- 测试更大计算量场景:比如批量处理多张图像或大尺寸图像,才能体现Numba的性能优势。
修改后的示例代码
import numpy as np from numba import njit import time from tensorflow.keras.datasets import cifar10 (X_train, Y_train), (X_val, Y_val) = cifar10.load_data() X_train = np.transpose(X_train, (0,3,1,2)) @njit(nopython=True) def cal_padding(input_arr, padding): output = np.zeros((input_arr.shape[0] + padding*2, input_arr.shape[1] + padding*2), dtype=input_arr.dtype) output[padding : padding + input_arr.shape[0], padding : padding + input_arr.shape[1]] = input_arr return output @njit(nopython=True) def conv_per_channel(data, kernel, padding=1, stride=1): h, w = data.shape padded_data = cal_padding(data, padding) kh, kw = kernel.shape h_output = (h - kh + 2*padding) // stride + 1 w_output = (w - kw + 2*padding) // stride + 1 transformed = np.zeros((h_output, w_output), dtype=data.dtype) # 直接遍历核坐标,避免额外列表存储 for i in range(h_output): for j in range(w_output): start_x = i * stride start_y = j * stride val = 0.0 for ki in range(kh): for kj in range(kw): val += padded_data[start_x + ki, start_y + kj] * kernel[ki, kj] # 裁剪值到0-255范围 val = min(max(val, 0), 255) transformed[i, j] = val return transformed # 预热:先调用一次完成编译 sobelX = np.array([[-1,0,1], [-2,0,2],[-1,0,1]]) conv_per_channel(X_train[0][0], sobelX) # 测试批量处理,体现Numba优势 start = time.time() for img in X_train[:500]: conv_per_channel(img[0], sobelX) end = time.time() print(f"Numba批量处理500张耗时: {end - start:.6f}秒") # 原生Python对比实现 def native_conv_per_channel(data, kernel, padding=1, stride=1): h, w = data.shape padded_data = np.pad(data, padding, mode='constant') kh, kw = kernel.shape h_output = (h - kh + 2*padding) // stride + 1 w_output = (w - kw + 2*padding) // stride + 1 transformed = np.zeros((h_output, w_output), dtype=data.dtype) for i in range(h_output): for j in range(w_output): start_x = i * stride start_y = j * stride patch = padded_data[start_x:start_x+kh, start_y:start_y+kw] val = np.sum(patch * kernel) val = min(max(val, 0), 255) transformed[i, j] = val return transformed start = time.time() for img in X_train[:500]: native_conv_per_channel(img[0], sobelX) end = time.time() print(f"原生Python批量处理500张耗时: {end - start:.6f}秒")
内容的提问来源于stack exchange,提问作者Tùng Nguyễn
相关产品推荐
相关产品推荐

