use alloc::collections::{BTreeMap, VecDeque};
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use crate::ir::Block;
use crate::jit::tlb::{Epoch, PAGE_MASK, PAGE_SIZE};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BlockId {
slot: u32,
stamp: u64,
}
pub const EXITS: usize = 2;
const NONE: u32 = u32::MAX;
#[derive(Debug, Clone, Copy)]
struct Link {
pc: u64,
target: u32,
stamp: u64,
}
impl Link {
const fn empty() -> Link {
Link {
pc: 0,
target: NONE,
stamp: 0,
}
}
}
#[derive(Debug)]
struct Slot {
block: Block,
pc: u64,
key: u64,
page: u64,
insns: usize,
stamp: u64,
next: u32,
exits: [Link; EXITS],
preds: Vec<(u32, u8)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub chained: u64,
pub links: u64,
pub unlinks: u64,
pub inserts: u64,
pub smc: u64,
pub evictions: u64,
pub flushes: u64,
pub filtered: u64,
pub stale_links: u64,
}
#[derive(Debug)]
pub struct BlockCache {
slots: Vec<Option<Slot>>,
next_stamp: u64,
free: Vec<u32>,
buckets: Vec<u32>,
bucket_mask: u64,
pages: BTreeMap<u64, Vec<u32>>,
filter: Vec<u64>,
order: VecDeque<(u32, u64)>,
capacity: usize,
epoch: Epoch,
stats: CacheStats,
}
pub const DEFAULT_CAPACITY: usize = 8192;
impl BlockCache {
#[must_use]
pub fn new() -> BlockCache {
BlockCache::with_capacity(DEFAULT_CAPACITY)
}
#[must_use]
pub fn with_capacity(capacity: usize) -> BlockCache {
let capacity = capacity.max(1);
let buckets = capacity.next_power_of_two();
let filter_words = (capacity / 4).clamp(8, 512);
BlockCache {
slots: Vec::new(),
next_stamp: 1,
free: Vec::new(),
buckets: vec![NONE; buckets],
bucket_mask: (buckets - 1) as u64,
pages: BTreeMap::new(),
filter: vec![0u64; filter_words],
order: VecDeque::new(),
capacity,
epoch: Epoch::default(),
stats: CacheStats::default(),
}
}
#[inline]
#[must_use]
pub fn stats(&self) -> CacheStats {
self.stats
}
#[must_use]
pub fn len(&self) -> usize {
self.slots.len() - self.free.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
#[must_use]
pub fn epoch(&self) -> Epoch {
self.epoch
}
pub fn sync(&mut self, epoch: Epoch) -> bool {
let flush = epoch.topology != self.epoch.topology;
self.epoch = epoch;
if flush {
self.flush();
}
flush
}
pub fn flush(&mut self) {
self.slots.clear();
self.free.clear();
self.buckets.fill(NONE);
self.pages.clear();
self.filter.fill(0);
self.order.clear();
self.stats.flushes += 1;
}
pub fn lookup(&mut self, pc: u64, key: u64) -> Option<BlockId> {
match self.find(pc, key) {
Some(id) => {
self.stats.hits += 1;
Some(id)
}
None => {
self.stats.misses += 1;
None
}
}
}
fn find(&self, pc: u64, key: u64) -> Option<BlockId> {
let mut at = self.buckets[self.bucket(pc, key)];
while at != NONE {
let slot = self.slots[at as usize].as_ref()?;
if slot.pc == pc && slot.key == key {
return Some(BlockId {
slot: at,
stamp: slot.stamp,
});
}
at = slot.next;
}
None
}
#[inline]
fn slot(&self, id: BlockId) -> Option<&Slot> {
self.slots
.get(id.slot as usize)?
.as_ref()
.filter(|s| s.stamp == id.stamp)
}
pub fn insert(&mut self, pc: u64, key: u64, page: u64, insns: usize, block: Block) -> BlockId {
if let Some(old) = self.find(pc, key) {
self.remove(old.slot);
}
while self.len() >= self.capacity {
if !self.evict_one() {
break;
}
}
let page = page & !PAGE_MASK;
let stamp = self.next_stamp;
self.next_stamp = self.next_stamp.wrapping_add(1);
let fresh = Slot {
block,
pc,
key,
page,
insns,
stamp,
next: NONE,
exits: [Link::empty(); EXITS],
preds: Vec::new(),
};
let id = match self.free.pop() {
Some(i) => {
self.slots[i as usize] = Some(fresh);
i
}
None => {
let i = u32::try_from(self.slots.len()).unwrap_or(NONE);
self.slots.push(Some(fresh));
i
}
};
let bucket = self.bucket(pc, key);
let head = self.buckets[bucket];
if let Some(slot) = self.slots[id as usize].as_mut() {
slot.next = head;
}
self.buckets[bucket] = id;
self.pages.entry(page).or_default().push(id);
self.mark_filter(page);
self.order.push_back((id, stamp));
self.stats.inserts += 1;
BlockId { slot: id, stamp }
}
#[inline]
#[must_use]
pub fn block(&self, id: BlockId) -> Option<&Block> {
self.slot(id).map(|s| &s.block)
}
#[inline]
#[must_use]
pub fn insns(&self, id: BlockId) -> Option<usize> {
self.slot(id).map(|s| s.insns)
}
#[inline]
#[must_use]
pub fn page(&self, id: BlockId) -> Option<u64> {
self.slot(id).map(|s| s.page)
}
pub fn link(&mut self, from: BlockId, pc: u64, to: BlockId) {
if self.slot(to).is_none() || self.slot(from).is_none() {
return;
}
let Some(slot) = self
.slots
.get_mut(from.slot as usize)
.and_then(|s| s.as_mut())
else {
return;
};
let mut chosen = None;
for (i, link) in slot.exits.iter().enumerate() {
if link.target != NONE && link.pc == pc {
if link.target == to.slot && link.stamp == to.stamp {
return;
}
chosen = Some(i);
break;
}
if link.target == NONE && chosen.is_none() {
chosen = Some(i);
}
}
let Some(i) = chosen else {
return;
};
let old = slot.exits[i];
slot.exits[i] = Link {
pc,
target: to.slot,
stamp: to.stamp,
};
if old.target != NONE {
self.drop_back_edge(old.target, old.stamp, from.slot, i as u8);
}
if let Some(target) = self
.slots
.get_mut(to.slot as usize)
.and_then(|s| s.as_mut())
{
target.preds.push((from.slot, i as u8));
}
self.stats.links += 1;
}
pub fn follow(&mut self, from: BlockId, pc: u64, key: u64) -> Option<BlockId> {
let link = *self
.slot(from)?
.exits
.iter()
.find(|l| l.target != NONE && l.pc == pc)?;
let id = BlockId {
slot: link.target,
stamp: link.stamp,
};
let Some(live) = self.slot(id) else {
if self
.slots
.get(link.target as usize)
.is_some_and(Option::is_some)
{
self.stats.stale_links += 1;
}
return None;
};
if live.pc != pc || live.key != key {
return None;
}
self.stats.chained += 1;
Some(id)
}
pub fn invalidate_page(&mut self, phys: u64) -> usize {
let page = phys & !PAGE_MASK;
let Some(ids) = self.pages.remove(&page) else {
return 0;
};
let n = ids.len();
for id in ids {
self.remove_keeping_page(id);
}
n
}
pub fn note_write(&mut self, phys: u64, len: u64) -> usize {
if len == 0 {
return 0;
}
let first = phys & !PAGE_MASK;
let last = phys.saturating_add(len - 1) & !PAGE_MASK;
let mut hit = 0;
let mut page = first;
loop {
if self.filter_set(page) {
hit += self.invalidate_page(page);
} else {
self.stats.filtered += 1;
}
if page >= last {
break;
}
page = page.saturating_add(PAGE_SIZE);
}
self.stats.smc += hit as u64;
hit
}
#[inline]
fn filter_set(&self, page: u64) -> bool {
let bit = filter_bit(page, self.filter.len());
self.filter[bit / 64] & (1u64 << (bit % 64)) != 0
}
#[inline]
fn mark_filter(&mut self, page: u64) {
let bit = filter_bit(page, self.filter.len());
self.filter[bit / 64] |= 1u64 << (bit % 64);
}
pub fn invalidate(&mut self, id: BlockId) -> bool {
if self.slot(id).is_none() {
return false;
}
self.remove(id.slot);
true
}
fn evict_one(&mut self) -> bool {
while let Some((id, stamp)) = self.order.pop_front() {
let live = self
.slots
.get(id as usize)
.and_then(|s| s.as_ref())
.is_some_and(|s| s.stamp == stamp);
if live {
self.remove(id);
self.stats.evictions += 1;
return true;
}
}
false
}
fn remove(&mut self, id: u32) {
let page = self
.slots
.get(id as usize)
.and_then(|s| s.as_ref())
.map(|s| s.page);
if let Some(page) = page
&& let Some(list) = self.pages.get_mut(&page)
{
list.retain(|x| *x != id);
if list.is_empty() {
self.pages.remove(&page);
}
}
self.remove_keeping_page(id);
}
fn remove_keeping_page(&mut self, id: u32) {
let Some(slot) = self.slots.get_mut(id as usize).and_then(|s| s.take()) else {
return;
};
for (i, link) in slot.exits.iter().enumerate() {
if link.target != NONE {
self.drop_back_edge(link.target, link.stamp, id, i as u8);
}
}
for (pred, i) in &slot.preds {
if let Some(p) = self.slots.get_mut(*pred as usize).and_then(|s| s.as_mut())
&& let Some(link) = p.exits.get_mut(*i as usize)
&& link.target == id
{
*link = Link::empty();
self.stats.unlinks += 1;
}
}
let bucket = self.bucket(slot.pc, slot.key);
let mut at = self.buckets[bucket];
if at == id {
self.buckets[bucket] = slot.next;
} else {
while at != NONE {
let next = match self.slots.get(at as usize).and_then(|s| s.as_ref()) {
Some(s) => s.next,
None => break,
};
if next == id {
if let Some(s) = self.slots.get_mut(at as usize).and_then(|s| s.as_mut()) {
s.next = slot.next;
}
break;
}
at = next;
}
}
self.free.push(id);
}
fn drop_back_edge(&mut self, target: u32, stamp: u64, pred: u32, i: u8) {
if let Some(t) = self.slots.get_mut(target as usize).and_then(|s| s.as_mut())
&& t.stamp == stamp
{
t.preds.retain(|(p, s)| !(*p == pred && *s == i));
}
}
#[inline]
fn bucket(&self, pc: u64, key: u64) -> usize {
let mut h = pc ^ key.wrapping_mul(0x9e37_79b9_7f4a_7c15);
h ^= h >> 29;
h = h.wrapping_mul(0xbf58_476d_1ce4_e5b9);
h ^= h >> 32;
(h & self.bucket_mask) as usize
}
pub fn check(&self) -> Result<(), String> {
use alloc::format;
for (i, slot) in self.slots.iter().enumerate() {
let Some(slot) = slot else { continue };
let i = i as u32;
for (n, link) in slot.exits.iter().enumerate() {
if link.target == NONE {
continue;
}
let Some(t) = self
.slots
.get(link.target as usize)
.and_then(|s| s.as_ref())
else {
return Err(format!("block {i} exit {n} points at a freed slot"));
};
if t.stamp != link.stamp {
return Err(format!("block {i} exit {n} points at a reused slot"));
}
if !t.preds.contains(&(i, n as u8)) {
return Err(format!("block {i} exit {n} has no matching back edge"));
}
}
for (pred, n) in &slot.preds {
let Some(p) = self.slots.get(*pred as usize).and_then(|s| s.as_ref()) else {
return Err(format!("block {i} has a back edge from a freed slot"));
};
let Some(link) = p.exits.get(*n as usize) else {
return Err(format!(
"block {i} has a back edge to exit {n}, which does not exist"
));
};
if link.target != i {
return Err(format!(
"block {i} has a back edge from {pred} exit {n}, which points elsewhere"
));
}
}
match self.pages.get(&slot.page) {
Some(list) if list.contains(&i) => {}
_ => return Err(format!("block {i} is not in its page's index")),
}
if !self.filter_set(slot.page) {
return Err(format!("block {i}'s page is not in the filter"));
}
}
for (page, list) in &self.pages {
for id in list {
match self.slots.get(*id as usize).and_then(|s| s.as_ref()) {
Some(s) if s.page == *page => {}
_ => return Err(format!("page {page:#x} indexes a block that is not there")),
}
}
}
if self.stats.stale_links != 0 {
return Err(format!(
"{} stale links were followed, so a back edge was not cleared",
self.stats.stale_links
));
}
Ok(())
}
}
impl Default for BlockCache {
fn default() -> BlockCache {
BlockCache::new()
}
}
#[inline]
fn filter_bit(page: u64, words: usize) -> usize {
let bits = (words * 64) as u64;
let mixed = (page >> 12).wrapping_mul(0x9e37_79b9_7f4a_7c15);
((mixed >> 32) % bits) as usize
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{BlockBuilder, Const, Type};
fn block(pc: u64) -> Block {
let mut b = BlockBuilder::new(pc, 0);
let _ = b.imm(Type::I64, Const::Int(u128::from(pc)));
b.exit_tb();
b.finish()
}
fn cache() -> BlockCache {
BlockCache::with_capacity(16)
}
#[test]
fn a_block_comes_back_under_its_own_key_and_no_other() {
let mut c = cache();
let id = c.insert(0x1000, 7, 0x1000, 1, block(0x1000));
assert_eq!(c.lookup(0x1000, 7), Some(id));
assert_eq!(
c.lookup(0x1000, 8),
None,
"a different key is a different block"
);
assert_eq!(c.lookup(0x1004, 7), None);
assert_eq!(c.block(id).map(|b| b.entry_pc), Some(0x1000));
c.check().expect("consistent");
}
#[test]
fn a_chained_exit_skips_the_lookup() {
let mut c = cache();
let a = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let b = c.insert(0x1004, 0, 0x1000, 1, block(0x1004));
c.link(a, 0x1004, b);
assert_eq!(c.follow(a, 0x1004, 0), Some(b));
assert_eq!(c.stats().chained, 1);
assert_eq!(c.follow(a, 0x2000, 0), None, "an unpatched exit");
c.check().expect("consistent");
}
#[test]
fn a_chain_link_is_cleared_when_its_target_is_invalidated() {
let mut c = cache();
let a = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let b = c.insert(0x1004, 0, 0x2000, 1, block(0x1004));
c.link(a, 0x1004, b);
assert_eq!(c.invalidate_page(0x2000), 1);
assert_eq!(c.follow(a, 0x1004, 0), None);
assert_eq!(c.stats().unlinks, 1);
assert_eq!(c.stats().stale_links, 0, "cleared, not merely detected");
c.check().expect("consistent");
}
#[test]
fn invalidating_a_predecessor_leaves_no_back_edge_behind() {
let mut c = cache();
let a = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let b = c.insert(0x1004, 0, 0x2000, 1, block(0x1004));
c.link(a, 0x1004, b);
c.invalidate(a);
c.check().expect("b's back edge went with a");
let d = c.insert(0x3000, 0, 0x3000, 1, block(0x3000));
assert_eq!(c.follow(d, 0x1004, 0), None);
c.check().expect("consistent");
}
#[test]
fn a_reused_slot_is_never_reached_through_an_old_link() {
let mut c = cache();
let a = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let b = c.insert(0x1004, 0, 0x2000, 1, block(0x1004));
c.link(a, 0x1004, b);
c.invalidate(b);
let e = c.insert(0x9000, 0, 0x9000, 1, block(0x9000));
assert_ne!(c.follow(a, 0x1004, 0), Some(e));
assert_eq!(c.stats().stale_links, 0);
c.check().expect("consistent");
}
#[test]
fn a_guest_write_into_a_page_holding_translations_invalidates_them() {
let mut c = cache();
let a = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let b = c.insert(0x1010, 0, 0x1000, 1, block(0x1010));
let elsewhere = c.insert(0x5000, 0, 0x5000, 1, block(0x5000));
assert_eq!(c.note_write(0x1008, 4), 2, "both blocks on the page went");
assert_eq!(c.lookup(0x1000, 0), None);
assert_eq!(c.lookup(0x1010, 0), None);
assert_eq!(c.lookup(0x5000, 0), Some(elsewhere));
let _ = (a, b);
c.check().expect("consistent");
}
#[test]
fn a_write_spanning_two_pages_invalidates_both() {
let mut c = cache();
c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
c.insert(0x2000, 0, 0x2000, 1, block(0x2000));
assert_eq!(c.note_write(0x1ffe, 8), 2);
c.check().expect("consistent");
}
#[test]
fn a_write_into_a_page_with_no_translations_costs_a_filter_test() {
let mut c = cache();
c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
assert_eq!(c.note_write(0x8_0000, 8), 0);
assert!(c.stats().filtered >= 1);
c.check().expect("consistent");
}
#[test]
fn a_topology_change_invalidates_every_cached_block() {
let mut c = cache();
c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
assert!(c.sync(Epoch {
topology: 1,
translation: 0
}));
assert_eq!(c.lookup(0x1000, 0), None);
assert!(c.is_empty());
}
#[test]
fn a_translation_generation_bump_leaves_bare_blocks_alone() {
let mut c = cache();
let bare = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
assert!(!c.sync(Epoch {
topology: 0,
translation: 9
}));
assert_eq!(c.lookup(0x1000, 0), Some(bare));
}
#[test]
fn the_cache_evicts_in_insertion_order_and_stays_consistent() {
let mut c = BlockCache::with_capacity(4);
let mut ids = Vec::new();
for n in 0..8u64 {
ids.push(c.insert(n * 0x1000, 0, n * 0x1000, 1, block(n * 0x1000)));
if n > 0 {
c.link(ids[(n - 1) as usize], n * 0x1000, ids[n as usize]);
}
c.check().expect("consistent at every step");
}
assert_eq!(c.len(), 4);
assert_eq!(c.lookup(0, 0), None, "the first went first");
assert_eq!(c.stats().evictions, 4);
assert_eq!(c.stats().stale_links, 0);
}
#[test]
fn reinserting_the_same_key_replaces_rather_than_duplicates() {
let mut c = cache();
let first = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let second = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
assert_ne!(first, second);
assert_eq!(c.len(), 1);
assert_eq!(c.lookup(0x1000, 0), Some(second));
c.check().expect("consistent");
}
#[test]
fn a_block_with_more_successors_than_exits_keeps_two_and_looks_the_rest_up() {
let mut c = cache();
let a = c.insert(0x9_0000, 0, 0x9_0000, 1, block(0x9_0000));
let targets: Vec<BlockId> = (1..4)
.map(|n| c.insert(n * 0x1000, 0, n * 0x1000, 1, block(n * 0x1000)))
.collect();
for (n, t) in targets.iter().enumerate() {
c.link(a, (n as u64 + 1) * 0x1000, *t);
}
let followed = (1..4)
.filter(|n| c.follow(a, n * 0x1000, 0).is_some())
.count();
assert_eq!(followed, EXITS);
c.check().expect("consistent");
}
#[test]
fn a_link_replaced_at_the_same_exit_pc_drops_the_old_back_edge() {
let mut c = cache();
let a = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let b = c.insert(0x2000, 0, 0x2000, 1, block(0x2000));
c.link(a, 0x2000, b);
c.invalidate(b);
let b2 = c.insert(0x2000, 0, 0x2000, 1, block(0x2000));
c.link(a, 0x2000, b2);
assert_eq!(c.follow(a, 0x2000, 0), Some(b2));
c.check().expect("consistent");
assert_eq!(c.stats().stale_links, 0);
}
#[test]
fn a_flush_leaves_nothing_pointing_anywhere() {
let mut c = cache();
let a = c.insert(0x1000, 0, 0x1000, 1, block(0x1000));
let b = c.insert(0x2000, 0, 0x2000, 1, block(0x2000));
c.link(a, 0x2000, b);
c.flush();
assert!(c.is_empty());
assert_eq!(c.lookup(0x1000, 0), None);
c.check().expect("consistent");
}
}