若内存映射变更时总会触发缓存失效,为何不选择VIVT缓存?
据我所知,至少在Intel芯片中L1缓存为VIPT类型。VIVT缓存不依赖地址转换,可与TLB查找完全并行执行。VIPT可通过不使用物理地址位作为组索引实现一定并行性,但确定组后,路查找必须等待TLB结果(除非有路预测器)。因此VIPT至少在一定程度上依赖TLB,而VIVT操作完全独立。
VIVT的主要问题是内存映射会随时间变化,每次变更都必须失效缓存(或部分缓存)。但VIPT也是如此吗?根据内核文档:
通常,当Linux将现有虚拟->物理映射修改为新值时,流程会采用以下形式之一:
1) flush_cache_mm(mm); change_all_page_tables_of(mm); flush_tlb_mm(mm); 2) flush_cache_range(vma, start, end); change_range_of_page_tables(mm, start, end); flush_tlb_range(vma, start, end); 3) flush_cache_page(vma, addr, pfn); set_pte(pte_pointer, new_pte_val); flush_tlb_page(vma, addr);
三种情况的第一步都是部分或完全缓存失效。切换到不同进程必然会改变虚拟->物理映射,因此我认为内核会执行缓存刷新。据我理解,即使同一进程内的映射发生变化,Linux也会失效(部分)L1缓存。既然中间已执行失效操作,VIVT的问题是否已解决,VIPT的优势是否也不复存在?
问题解答
首先要明确:VIVT的核心问题并没有被完全解决,VIPT的优势依然存在,核心原因主要有这几点:
缓存失效的开销差异
VIVT缓存的失效粒度往往更大——因为虚拟地址可能被多个进程共享,或是同一进程内不同映射复用虚拟地址,一旦映射变更,可能需要失效更大范围的缓存,甚至整个缓存。而VIPT缓存由于组索引不依赖物理地址,路查找才需要TLB结果,映射变更时只需要针对特定虚拟地址范围失效缓存就行,开销更小。Linux的缓存失效操作虽然统一做了处理,但VIVT本身的特性决定了它的失效操作开销天生就比VIPT大。进程切换的额外开销
用VIVT缓存的话,进程切换时,不同进程的虚拟地址空间完全独立,哪怕物理地址没冲突,同一虚拟地址对应的缓存项也会失效(新进程的同一虚拟地址可能映射到不同物理地址),所以必须全量或大范围失效缓存。而VIPT缓存的组索引用的是虚拟地址的低比特位(这部分在地址转换时不会变,虚拟地址的页内偏移和物理地址一致),进程切换时不需要全量失效缓存,只刷新TLB就行,这能大幅降低进程切换的缓存开销,提升系统整体性能。并行性的实际表现差异
你提到VIVT能和TLB完全并行,VIPT只能部分并行,但这只是理想情况。实际场景中,VIVT的并行性优势会被频繁的缓存失效抵消——每次映射变更或进程切换都要失效缓存,导致缓存命中率下降,反而拖慢整体性能。而VIPT虽然路查找依赖TLB,但通过路预测器等优化,大部分场景下能接近VIVT的并行性,同时还能避免VIVT的缓存失效开销。硬件实现的复杂度差异
VIVT缓存要处理虚拟地址冲突的问题(同一虚拟地址映射到不同物理地址时,缓存会出现歧义),硬件上需要额外逻辑来处理;而VIPT通过设计(组索引用虚拟地址的页内偏移部分,这部分和物理地址一致)避免了这种歧义,硬件实现更简单可靠。
总结来说,Linux的缓存失效机制只是缓解了VIVT的部分问题,但并没有从根本上解决VIVT固有的缺陷,VIPT在缓存失效开销、进程切换性能、硬件实现复杂度等方面依然有明显优势,这也是Intel等主流厂商选择VIPT作为L1缓存类型的核心原因。
内容的提问来源于stack exchange,提问作者Devashish

