Skip to content

缓存

在许多情况下,Bodo 可以将函数编译生成的二进制文件保存到磁盘,以便在未来的运行中重用。这避免了下次运行应用程序时重新编译函数的需要。

仅在使用新输入类型调用函数时,才有必要重新编译函数,缓存也适用同样的原则。换句话说,如果数据类型保持不变(这是最常见的情况),则可以多次运行一个应用程序并处理不同的数据,而不必重新编译任何代码。

警告

缓存在大多数(但不是全部)情况下有效,并且默认情况下是禁用的。有关更多信息,请参阅下方的缓存限制。

缓存示例

要缓存一个函数,我们只需要将选项 cache=True 添加到 JIT 装饰器中:

import time
import pandas as pd
import bodo


@bodo.jit(cache=True)
def mean_power_speed():
    df = pd.read_parquet("data/cycling_dataset.pq")
    return df[["power", "speed"]].mean()


t0 = time.time()
result = mean_power_speed()
print(result)
print("Total execution time:", round(time.time() - t0, 3), "secs")

上述代码第一次运行时,Bodo 编译该函数并将其缓存到磁盘。该代码计算整个函数调用的时间,这包括第一次运行函数时的编译时间:

power    102.078421
speed      5.656851
dtype: float64
Total execution time: 4.614 secs
在后续运行中,它将从缓存中恢复函数,因此执行时间将大大缩短:

power    102.078421
speed      5.656851
dtype: float64
Total execution time: 0.518 secs

注意

data/cycling_dataset.pq 位于 Bodo 教程 repo 中。

缓存位置和可移植性

在大多数情况下,缓存保存在源文件所在目录中的 __pycache__ 目录中。可以将变量 NUMBA_DEBUG_CACHE 设置为 1,以查看缓存的确切位置以及是否正在写入或读取。

在Jupyter笔记本中,缓存目录被称为 numba_cache,位于 IPython.paths.get_ipython_cache_dir()。有关这些和其他备用缓存位置的更多信息,请参见 here。例如,在笔记本中运行时:

import os
import IPython


cache_dir = IPython.paths.get_ipython_cache_dir() + "/numba_cache"
print("Cache files:")
os.listdir(cache_dir)
Cache files:
['ipython-input-bce41f829e09.mean_power_speed-4444615264.py38.nbi',
'ipython-input-bce41f829e09.mean_power_speed-4444615264.py38.1.nbc']

缓存对象在具有相同CPU型号和CPU特征的系统之间有效。因此,在不同机器上共享和重用缓存目录中的内容是安全的。有关更多信息,请参见这里

缓存失效

当相应的源代码被修改时,缓存会自动失效。观察这种行为的一种方法是,在首次缓存后修改上述示例,改变变量名 df。下次运行代码时,Bodo 将确定源代码已被修改,失效缓存并重新编译该函数。

警告

有时需要手动清除缓存(请参见下面的缓存限制)。要清除缓存,只需删除缓存文件。

重用缓存的提示

如上所述,缓存会在文件中任何源代码发生变化时使函数失效。如果我们在同一个文件中定义一个函数并调用它,并修改传递给函数的参数,缓存将会失效。

缓存文件输入输出

例如:一个典型的使用案例是调用一个输入输出函数,使用不同的文件名。

@bodo.jit(cache=True)
def io_call(file_name):
    ...
io_call("mydata.parquet")

如果io_call的参数从mydata.parquet发生更改,则需要对上述函数进行重新编译。通过将函数调用与函数定义分开,函数定义不需要针对每个具有新参数的函数调用进行重新编译。缓存的IO函数将在文件名称发生更改时工作,只要文件模式相同。例如,下面的代码片段

import IO_function from IO_functions
IO_function(file_name)

每次 file_name 被修改时,不需要重新编译 IO_function,因为 IO_function 与该代码更改是隔离的。

缓存笔记本单元格

对于IPython笔记本,应该将要缓存的函数与函数调用放在不同的单元格中。

@bodo.jit(cache=True)
def io_call(file_name):
    ...
io_call(file_name)
io_call(another_file_name)
...

如果一个包含缓存函数的单元格被修改,那么它的缓存将失效,函数必须重新编译。

当前缓存限制

  • 编译函数的更改在不同文件之间是不可见的。比如,如果我们有一个缓存的 Bodo 函数,该函数调用了一个在不同文件中的缓存 Bodo 函数,并且修改了后者,那么 Bodo 将不会更新其缓存(因此将使用该函数的旧版本运行)。
  • 全局变量被视为编译时常量。当一个 函数被编译时,函数使用的任何全局变量的值将在编译时嵌入到二进制文件中,并保持不变。如果全局变量的值在编译后在源代码中发生变化,则编译后的对象(和缓存)将不会重新绑定到新值。

故障排除

在执行过程中,如果环境变量 NUMBA_DEBUG_CACHE 被设置为 1,Bodo 将会打印缓存信息。例如,在第一次运行时,它将显示缓存是否被保存以及保存的位置,而在随后的运行中,它将显示编译器是否成功从缓存中加载。

如果编译器报告无法缓存一个函数,或者无法从缓存加载一个函数,请在我们的仓库中报告这个问题。