1. 引言
在使用地平线开发板进行模型部署时,大家会遇到两个函数hbSysAllocCachedMem() 和 hbSysAllocMem(),他们都是用来申请内存的,可以发现他们的区别只有一个cache,此时,不知道大家有没有这些疑问:
- 什么是cache?
- 为什么要用cache?
- 怎么用cache?
带着这些疑问,我们来一起学习cache机制。
2. 什么是cache
cache,中文名缓存,是一种可以进行高速数据交换的存储器,常用于CPU与内存memory之间。从数据交互内容看,缓存是内存中少部分数据的复制品,cache与内存之间的数据交互称为cache刷新;从数据交互速度看,CPU与cache之间的数据交互远快于与CPU与内存之间的数据交互。-
3. 为什么要用cache
程序运行时,CPU从内存中读写入指令、数据,但是直接从内存中读写是很慢的,所以就引入了可以和CPU之间高速数据交换的缓存cache。cache机制的工作原理如下:
- 当CPU需要读取一个数据时,首先从缓存cache中查找,找到就立即送给CPU处理。数据已经在缓存中,用一个专业名词代替:缓存命中(cache hit),此时CPU可以直接从缓存中读取数据,从而大大加快访问速度。
- 如果数据不在缓存中,专业名词称为:缓存未命中(cache miss)。在这种情况下,CPU需要从速率相对较慢的内存中加载数据到缓存中,然后再送给CPU进行处理。这个过程涉及较慢的内存访问,因此会引起一些延迟。
缓存命中和缓存未命中一起形成了缓存命中率的概念,命中率对于cache而言是很重要的,因为CPU读取数据的顺序是先缓存后内存,且缓存的刷新只刷新发生变化的数据。
那什么场景下推荐使用带cache的内存呢?
- CPU频繁读取数据的场景。
- 模型连续推理时,多次读写输入/输出数据的场景。
以上场景均可以充分发挥缓存命中的优势,大幅提升性能。此外,对于不确定的场景,也建议使用cache。
此时,大家可能有这样的疑问,既然cache这么好,是不是所有场景都是用cache更优呢?
地平线计算平台中计算器件有CPU、BPU等,它们之间是共享内存的,如上图所示,如果输入数据仅给BPU进行处理,可以不使用cache,减少cache数据刷新的时间开销(1M数据约60us)。
4. 怎么用cache
先来了解一些接口函数。
4.1 hbSysAllocCachedMem()
申请带缓存的内存。
int32_t hbSysAllocCachedMem(hbSysMem *mem, uint32_t size);
- 参数
- [in] size 申请内存的大小。
- [out] mem 内存指针。
- 返回值
- 返回 0 则表示API成功执行,否则执行失败。
4.2 hbSysFlushMem()
对缓存和内存数据进行刷新。
int32_t hbSysFlushMem(hbSysMem *mem, int32_t flag);
- 参数
- [in] mem 内存指针。
- [in] flag 刷新标志符,有1和2两个参数来控制数据刷新方向,详情见hbSysMemFlushFlag的介绍。
- 返回值
- 返回 0 则表示API成功执行,否则执行失败。
4.3 hbSysMemFlushFlag
内存与缓存同步参数。
typedef enum {
HB_SYS_MEM_CACHE_INVALIDATE = 1,
HB_SYS_MEM_CACHE_CLEAN = 2
} hbSysMemFlushFlag;
- HB_SYS_MEM_CACHE_INVALIDATE 将内存中数据同步到缓存中,CPU读前使用,否则CPU会读取到之前缓存中的旧数据。
- HB_SYS_MEM_CACHE_CLEAN 将缓存中数据同步到内存中,CPU写后使用,否则BPU会读取到之前内存中的旧数据。
注意:其定义在hb_sys.h中,使用时记得include。
CPU与内存之间存在cache缓存区,若没有正确刷新数据,可能会导致缓存中的内容与内存中的内容不同步。为了每次都能够拿到最新的数据,我们需要在CPU读前、写后进行数据更新。CPU读前,将内存中数据更新到缓存中。CPU写后,将缓存中数据更新到内存中。
4.4 代码示例
以下代码节选自OE1.1.62中ddk/samples/ai_toolchain/horizon_runtime_sample/code/00_quick_start/src/run_mobileNetV1_224x224.cc
。
// define variables
std::vector<hbDNNTensor> input_tensors;
std::vector<hbDNNTensor> output_tensors;
hbDNNTensor *input = input_tensor;
int input_memSize = input[i].properties.alignedByteSize;
hbDNNTensor *output = output_tensor;
int output_memSize = output[i].properties.alignedByteSize;
// prepare input and output tensor
hbSysAllocCachedMem(&input[i].sysMem[0], input_memSize);
hbSysAllocCachedMem(&output[i].sysMem[0], output_memSize);
// make sure memory data is flushed to DDR before inference
hbSysFlushMem(&input_tensors[i].sysMem[0], HB_SYS_MEM_CACHE_CLEAN);
// run inference
hbDNNInfer(&task_handle,
&output,
input_tensors.data(),
dnn_handle,
&infer_ctrl_param);
// make sure CPU read data from DDR before using output tensor data
hbSysFlushMem(&output_tensors[i].sysMem[0], HB_SYS_MEM_CACHE_INVALIDATE);
4.5 hbSysAllocMem()
申请内存,此时不需要和cache之间存在数据交互,意味着不需要考虑hbSysAllocMem()与hbSysFlushMem()的联合使用。
int32_t hbSysAllocMem(hbSysMem *mem, uint32_t size);
- 参数
- [in] size 申请内存的大小。
- [out] mem 内存指针。
- 返回值
- 返回 0 则表示API成功执行,否则执行失败。