numpystd函数 numpy函数中循环语句处理数据
在Numba优化代码时,添加高效的break语句有时会导致严重的性能下降,甚至比不使用break的版本慢数倍。这主要是因为Numba基本依赖的LLVM编译器在break时存在难以进行分割块处理(SIMD优化),导致代码从的CPU处理中断为低效的标量处理。此外,分支预测拓扑异构性严重性能问题。本文将深入探讨这一现象的根源,并提供一种通过分块实处理的方法优化的策略。Numba 中的性能恢复现象
numba 是一个即时(jit)编译器,可以将 python 代码编译为快速的机器码,尤其要熟练处理处理数值计算。然而,在某些情况下,进行合理的优化(例如,为了提前退出循环而添加中断语句)反而会导致性能恢复。
考虑以下两个 Numba 函数,它们的目标是检查阵列中是否位于特定范围内的值:import numbaimport numpy as npfrom timeit import timeit@numba.njitdef count_in_range(arr, min_value, max_value): quot;quot;quot;计算队列中在指定范围内的元素数量,遍历整个队列。quot;quot;quot;count = 0 for a in arr:if min_value lt;a lt;max_value:count = 1 return count@numba.njitdef count_in_range2(arr,min_value,max_value):quot;quot;quot;检查队列中是否存在在指定范围内的元素,找到后立即退出。
quot;quot;quot; count = 0 for a in arr: if min_value lt; a lt; max_value: count = 1 break # lt;---- Break here return count# 基准测试代码def run_benchmark(): rng = np.random.default_rng(0) arr = rng.random(10 * 1000 * 1000) #选择一个不触发早期退出的条件,以确保公平比较循环遍历整个集群的情况 min_value = 0.5 max_value = min_value - 1e-10 #确保范围为空,不会触发if条件assert not np.any(np.逻辑_and(min_value lt;= arr, arr lt;= max_value)) n = 100 print(quot;---初步基准测试 ---quot;) for f in (count_in_range, count_in_range2): f(arr, min_value, max_value) # 预约JIT elapsed = timeit(lambda: f(arr, min_value, max_value), number=n) / n print(fquot;{f.__name__}: {elapsed * 1000:.3f} msquot;)# run_benchmark()登录后复制
开源基准测试结果示例:count_in_range: 3.351 mscount_in_range2: 42.312 ms登录后复制
令人惊讶的是,添加了break语句的count_in_range2函数在某些情况下比count_in_range慢了十倍以上。这与我们期望的提前退出带来的性能提升背道而驰。根源分析:LLV M向量化失效与分支预测
Numba通过将Python代码转换为LLVM中间表示(IR),然后利用LLVM工具链生成优化的机器码。LLVM在优化过程中会尝试进行多种向量化优化,其中一项关键技术是循环化。 LLVM支持化(SIMD)失效
支持化是指编译器将单个数据元素的操作转换为对多个数据元素同时进行操作的指令(SIMD,单指令,多指令)例如,一个SIMD指令可以处理4个或8个浮点数,显着提升同时计算密集型任务的性能。
当循环中中断语句时,LLVM编译器很难存在静态地确定循环的迭代次数。由于无法确定循环时会提前结束,编译器安全启动循环转换为无法的SIMD指令。结果,代码会不再为标量操作,即每次循环迭代只处理一个数据元素,这比简化操作效率低。
通过C编译器(同样基于LLVM)的编译输出可以清楚地看到这一点:无中断的循环:生成的编译代码会包含vmovupd,vcmpltpd, vandpd等SIMD指令,这些指令能够加载多个数据(例如,16个双精度浮点数)。有中断的循环:生成的代码会包含vmovsd等标量指令,每次只处理一个数据,导致性能急剧下降。
LLVM的诊断信息也证实了这一点:使用编译标志-Rpass-analysis=loop-vectorize,LLVM会报告“loop not vectorized:无法确定循环数” iterations”(循环未支持化:无法确定循环迭代次数)。2. 分支预测的影响
分支预测的影响
还会带来另一个性能缺陷:预测分支偏差。现代CPU通过预测如果或循环分支的转向来避免停止停顿。如果预测正确,程序正常执行;除了预测错误,CPU需要清空并重新存在加载正确的分支,这会带来显着的性能开销。降重鸟
分支效果好,就用降重。AI改写智能降低AIGC率和重复率。 113查看详情
在count_in_range2函数中,如果if min_value lt; a lt; max_value条件很少满足(例如,搜索范围非常小或数据分布使得匹配项稀少),CPU会倾向预测条件为假,继续循环。然而,当条件最终满足并触发中断时,CPU的预测就会失败,导致性能惩罚。
实验进一步数据验证对分支预测的影响:以下基准测试显示了count_in_range2在不同min_value下(即不同条件满足概率下)的性能变化,以及数据排列对分支预测的影响。# ... (Numba函数定义同上) ...defpartition(arr,threshold): quot;quot;将吞吐量元素分为小于阈值和大于等于阈值两部分,并拼接。quot;quot;less = arr[arr lt;threshold] more = arr[~(arr lt;threshold)] return np.concatenate((less, more))defpartition_with_error(arr,threshold, error_rate): quot;quot;quot;在分区的基础上引入错误率,打乱部分元素以增加分支预测分量。
quot;quot;quot; less = arr[arr lt;threshold] more = arr[~(arr lt;threshold)] # 引入错误,将一部分小于阈值的元素混入大于阈值的部分,反之亦然 less_error, less_ Correct = np.split(less, [int(len(less) * error_rate)]) more_error, more_ Correct = np.split(more, [int(len(more)) * error_rate)])mostly_less = np.concatenate((less_ Correct,more_error))mostly_more = np.concatenate((more_ Correct,less_error))rng = np.random.default_rng(0)rng.shuffle(mostly_less)rng.shuffle(mostly_more)out = np.concatenate((mostly_less,mostly_more))assert np.array_equal(np.sort(out), np.sort(arr)) # 确保元素不变 return outdef bench(f, arr, min_value, max_value, n=10, info=quot;quot;): f(arr, min_value, max_value) # 预热JIT elapsed = timeit(lambda: f(arr, min_value, max_value), number=n) / n print(fquot;{f.__name__}: {elapsed * 1000:.3f} ms, min_value: {min_value:.1f}, {info}quot;)def main_benchmark(): rng = np.random.default_rng(0) arr = rng.random(10 * 1000 * 1000) thresholds = np.linspace(0, 1, 11) print(quot;\n# --- 随机数据 ---quot;) for min_value in Thresholds: bench( count_in_range2, arr, min_value=min_value, max_value=min_value - 1e-10, # 确定范围为空 ) print(quot;\n# --- 分区数据(仍然是随机的)---quot;) for min_value in attempts: bench( count_in_range2,partition(arr,threshold=min_value),
min_value=min_value, max_value=min_value - 1e-10, ) print(quot;\n# --- 概率错误的已分区数据 ---quot;) forratio in links: bench( count_in_range2,partition_with_error(arr,threshold=0.5, error_rate=ratio), min_value=0.5, max_value=0.5 - 1e-10, #确定范围为空info=fquot;错误: {ratio:.0}quot;, )# main_benchmark()登录后复制
实验结果摘要:随机数据:count_in_range2的性能随min_value(即条件为真概率)变化,当min_value接近0.5时(条件真概率假各半,最难预测),性能最差数据:当数据阈值分区后,min_value如何,count_in_range2的性能相对稳定且较快。这是因为数据分区,分支预测的准确率提高极大。带有概率错误的要么按照分区数据:随着错误率(即分支预测)的增加,count_in_range2的性能逐渐下降,并在错误率50时达到最慢,再次验证了分支预测的重要性。解决方案:分块处理与手动管理策略
为了解决中断语句导致的支持化失效问题,我们采用一种分块处理(Chunking)的策略。其核心思想是把大集群划分为固定大小的小容量块,对每个小块进行处理。由于每个小块的大小是固定的,LLVM可以对其进行优化优化。同时,我们可以在处理完每个小块后检查是否需要提前退出,从而兼顾效率和提前终止的需求。
以下是一个优化后的Numba函数示例:@numba.njitdef count_in_range_faster(arr, min_value, max_value): quot;quot;quot;通过分块处理优化,实现类似提前退出但支持提示化的查找。如果找到则返回1,如果未找到则返回0。
quot;quot;quot; count = 0 # 设定一个块大小,例如16,常见的SIMD宽度(双精度浮点数) chunk_size = 16 for i in range(0, arr.size, chunk_size): # 处理的完整块 if arr.size - i gt;= chunk_size: # 一个视图来处理当前块,LLVM可以对固定大小的循环进行处理 tmp_view = arr[i : i chunk_size] for j in range(chunk_size): # 循环固定次数 if min_value lt; tmp_view[j] lt; max_value: count = 1 if count gt; 0: # 检查当前块是否找到,如果找到则提前返回 return 1 else: # 处理剩余的、不足一个完整块的元素 for j in range(i, arr.size): if min_value lt; arr[j] lt; max_value: count = 1 if count gt; 0: 返回1 返回0 #遍历完所有元素从而找到登录后复制
在这个count_in_range_faster函数中:我们使用一个外层循环以chunk_size为步长复制备份。对于每个大小为chunk_size的完整块,我们使用一个内层循环遍历其所有元素。由于这个内层循环的迭代次数是固定的(chunk_size),LLVM可以安全地进行缓存化优化,生成SI MD指令。在处理完每个后面,我们检查count大于0。如果找到了匹配项,就立即返回1,实现提前退出的逻辑。对于补足不足一个完整块的剩余元素,我们采用一个固定循环进行处理。
性能对比结果:在实际测试中,这种分块优化策略能够显着着提升性能,甚至超越块最初没有break的count_in_range函数。count_in_range: 7.112 mscount_in_range2: 35.317 mscount_in_range_faster: 5.827 毫秒 lt;----------后复制
可以,count_in_range_faster的性能明显超过count_in_range2,甚至比count_in_range还要快,因为它结合了支持化和早期退出的优势。总结与注意事项Numba与LLVM的良好作用:Numba的性能优势很大程度上来源于其对LLVM的利用。LLVM的限制优化(例如对理解中断语句的支持化限制)对于编写高性能的Numba代码优先。
中断语句的权衡:在Numba中,中断语句虽然能实现逻辑上的提前退出,但可能以牺牲底层保护化为代价。在性能敏感的循环中,需要仔细权衡其利弊。分块处理策略:当需要提前退出且循环体可以保护时,分块处理是一种有效的优化手段。它允许LLVM对固定大小的块进行保护,同时保持了提前退出的灵活性。分支优化预测:除了代码结构,数据排列和条件判断的概率也影响性能。分支使分支预测变得容易(例如,通过预排序数据),可以进一步提升性能。inspect_llvm()的用途:对于复杂的Numba函数,可以使用function.inspect_llvm()方法查看Numba生成的LLVM IR,从而理解编译器如何处理代码,并找出潜在的性能瓶颈。
通过理解Numba底层的工作原理和LLVM的优化,开发者可以更有效地编写高性能的Python数值计算限制代码。
以上就是Numba函数中中断语句导致性能恢复的深入分析与优化的详细内容,更多请关注哥通知网其他相关文章!功能