为什么numba生成的用于向量加法的LLVM IR太复杂了

问题描述 投票:0回答:1

我想检查 LLVM IR 是否有来自 numba 的向量加法,并注意到它只是为了一个简单的加法而生成大量 IR。我希望有一个简单的“添加”IR,但它生成了 LLVM IR 的2000 行。有没有办法获得最少的代码?

from numba import jit
import numpy as np

@jit(nopython=True,nogil=True)
def mysum(a,b):
    return a+b

a, b = 1.3 * np.ones(5), 2.2 * np.ones(5)
mysum(a, b)

# Get the llvm IR
llvm_ir =list(mysum.inspect_llvm().values())[0]
print(llvm_ir)
with open("llvm_ir.ll", "w") as file:
    file.write(llvm_ir)

# Get the assembly code
asm = list(mysum.inspect_asm().values())[0]
print(asm)

with open("llvm_ir.asm", "w") as file:
    file.write(asm)
llvm numba jit llvm-ir llvmlite
1个回答
0
投票

Numba 生成 3 个函数。第一个进行实际计算。第二个是一个包装函数,旨在从 CPython 调用。它将输入值的 CPython 动态对象转换为本机类型,并对返回值执行相反的操作。最后一个函数是从其他 Numba 函数(如果有)调用的。

转换 Numpy 数组并不是一项简单的任务(Numpy 数组是动态对象,包含一堆信息,如内存缓冲区、维度数、每个维度的步长 + 大小、动态 Numpy 类型等)。这就是为什么 Numpy 数组的代码比浮点值等更简单的数据类型的代码要大得多。事实上,在这种情况下,整个 LLVM IR 代码小了 20 倍,而且这个包装函数非常简单。

不过,主要问题并不是包装函数,而是第一个执行实际计算的函数(LLVM IR 代码的 75%)。原因之一是

a + b
创建一个新的临时 Numpy 数组,应使用隐式循环对其进行初始化和填充。与手动完成代码相比,此隐式操作会生成更多代码。这当然是因为 Numba 需要考虑许多在实践中可能永远不会发生的可能情况。例如,以下 Numba 函数的 LLVM IR 小两倍:

@jit('float64[::1](float64[::1], float64[::1])', nopython=True,nogil=True)
def mysum(a,b):
    out = np.empty(a.size, dtype=np.float64)
    for i in range(a.size):
        out[i] = a[i] + b[i]
    return out

如果我们去掉循环,那么它又会小两倍。这表明 Numpy 数组创建/初始化占用了很大一部分代码空间。该循环还占用大量空间,因为 Numba 需要支持 Numpy 数组支持的环绕功能,而且 Numpy 数组没有类型化数据缓冲区。在 C 中,数组和指针要简单得多,并且没有环绕。

在高级语言中,生成相当大的 IR/ASM 代码是很常见的。由于高级功能、代码大小优化不佳,代码通常很大。减少生成代码的大小是一项重要的工作,有时会与性能发生冲突。事实上,为了获得高性能代码,编译器通常需要展开循环,将代码拆分为不同的变体,以减轻更高级别功能(例如指针别名、向量化、消除环绕)的成本,从而导致显着更大的 IR/ASM代码。

© www.soinside.com 2019 - 2024. All rights reserved.