use super::program::{A1, BACKENDS, T0, T1, T2, config, ending_in_spin, run_to_pc, system_with};
use super::{Recorder, accounting_checks};
use crate::config::{
BackendKind, CacheConfig, Config, InclusionPolicy, PrefetcherKind, StorePrefetcherConfig,
};
use crate::isa::encoding::rv64i::{funct3 as i_f3, opcodes as i_op};
use crate::isa::encoding::zicboz::CBO_CLEAN_IMM;
use crate::tests::support::builder::instruction::{FENCE_IORW, InstructionBuilder};
use crate::tests::support::harness::TestContext;
const CACHES: [&str; 4] = ["core0.cache.l1i", "core0.cache.l1d", "core0.cache.l2", "llc"];
const SETTLE: u64 = 3_000;
fn sized(cache: &mut CacheConfig, bytes: usize, ways: usize) {
cache.enabled = true;
cache.size_bytes = bytes;
cache.ways = ways;
}
fn hierarchy(backend: BackendKind) -> Config {
let mut config = config(backend);
sized(&mut config.cache.l1_i, 2048, 2);
sized(&mut config.cache.l1_d, 1024, 1);
sized(&mut config.cache.l2, 4096, 4);
sized(&mut config.cache.l3, 16 * 1024, 8);
config
}
fn prefetching_hierarchy(backend: BackendKind) -> Config {
let mut config = hierarchy(backend);
for cache in
[&mut config.cache.l1_i, &mut config.cache.l1_d, &mut config.cache.l2, &mut config.cache.l3]
{
cache.prefetcher = PrefetcherKind::NextLine;
}
config.cache.store_prefetcher = StorePrefetcherConfig::Stream { streams: 4, l2_lines: 2 };
config
}
fn read_modify_write_lines(lines: i32) -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin(vec![
i().addi(T1, 0, lines).build(),
i().ld(T2, A1, 0).build(),
i().add(A1, A1, T2).build(),
i().addi(T2, T2, 1).build(),
i().sd(A1, T2, 0).build(),
i().addi(A1, A1, 64).build(),
i().addi(T1, T1, -1).build(),
i().bne(T1, 0, -24).build(),
])
}
fn run_settled(config: &Config, (program, end): (Vec<u32>, u64), context: &str) -> TestContext {
let mut ctx = system_with(config, &program, &[]);
run_to_pc(&mut ctx, end, context);
ctx.run(SETTLE);
ctx
}
fn held_lines(ctx: &TestContext, cache: &str) -> u64 {
let units = &ctx.sim.state.cores[0].units;
let held = match cache {
"core0.cache.l1i" => units.l1_i_cache.held_lines(),
"core0.cache.l1d" => units.l1_d_cache.held_lines(),
"core0.cache.l2" => units.l2_cache.held_lines(),
_ => ctx.sim.state.uncore.l3_cache.held_lines(),
};
held.len() as u64
}
fn check_settled_cache(rec: &mut Recorder, ctx: &TestContext, cache: &str, context: &str) -> u64 {
let sim = &ctx.sim;
let mut stat = |name: &str| rec.read(sim, &format!("{cache}.{name}"));
let (hits, misses, mshr_hits) = (stat("hits"), stat("misses"), stat("mshr_hits"));
let (fills, evictions) = (stat("fills"), stat("evictions"));
let issued = stat("prefetches.issued");
let (late, useful, unused) =
(stat("prefetches.late"), stat("prefetches.useful"), stat("prefetches.unused"));
let used = stat("prefetches.used");
let fetches = misses - mshr_hits + issued;
assert_eq!(fills, fetches, "{context}: {cache}: every fetch fills once");
assert_eq!(fills - evictions, held_lines(ctx, cache), "{context}: {cache}: lines held");
assert_eq!(used, late + useful, "{context}: {cache}");
assert!(late + useful + unused <= issued, "{context}: {cache}: prefetches resolved once");
let accuracy = if issued == 0 { 0.0 } else { used as f64 / issued as f64 };
rec.expect_ratio(sim, &format!("{cache}.prefetches.accuracy"), accuracy, context);
let rate = if hits + misses == 0 { 0.0 } else { misses as f64 / (hits + misses) as f64 };
rec.expect_ratio(sim, &format!("{cache}.miss_rate"), rate, context);
fetches
}
fn check_hierarchy(rec: &mut Recorder, ctx: &TestContext, context: &str) {
let [l1i, l1d, l2, _] = CACHES.map(|cache| check_settled_cache(rec, ctx, cache, context));
let requests = |rec: &mut Recorder, cache: &str| {
rec.read(&ctx.sim, &format!("{cache}.hits"))
+ rec.read(&ctx.sim, &format!("{cache}.misses"))
};
assert_eq!(requests(rec, "core0.cache.l2"), l1i + l1d, "{context}: the L2's requests");
assert_eq!(requests(rec, "llc"), l2, "{context}: the LLC's requests");
}
fn every_level_fills_each_fetch_once_and_holds_what_it_did_not_evict(rec: &mut Recorder) {
for backend in BACKENDS {
for (config, label) in [
(hierarchy(backend), "no prefetching"),
(prefetching_hierarchy(backend), "prefetching"),
] {
let context = format!("{backend:?} {label}");
let ctx = run_settled(&config, read_modify_write_lines(48), &context);
check_hierarchy(rec, &ctx, &context);
let evicted = rec.read(&ctx.sim, "core0.cache.l1d.evictions");
assert!(evicted > 0, "{context}: 48 lines through a 16-line L1D");
}
}
}
fn two_lines() -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
let mut program = Vec::new();
for offset in [0, 8, 64, 0] {
program.push(i().ld(T2, A1, offset).build());
program.push(i().add(A1, A1, T2).build());
}
ending_in_spin(program)
}
fn a_cold_line_misses_once_and_then_hits(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let ctx = run_settled(&hierarchy(backend), two_lines(), &context);
rec.expect(&ctx.sim, "core0.cache.l1d.misses", 2, &context);
rec.expect(&ctx.sim, "core0.cache.l1d.hits", 2, &context);
rec.expect(&ctx.sim, "core0.cache.l1d.mshr_hits", 0, &context);
rec.expect(&ctx.sim, "core0.cache.l1d.fills", 2, &context);
}
}
fn dirty_conflict() -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin(vec![
i().addi(T0, 0, 5).build(),
i().sd(A1, T0, 0).build(),
i().lui(T1, 0).build(),
i().addi(T1, T1, 1024).build(),
i().add(T1, A1, T1).build(),
i().ld(T2, T1, 0).build(),
])
}
fn a_dirty_victim_is_evicted_and_written_back(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let ctx = run_settled(&hierarchy(backend), dirty_conflict(), &context);
rec.expect(&ctx.sim, "core0.cache.l1d.evictions", 1, &context);
rec.expect(&ctx.sim, "core0.cache.l1d.writebacks", 1, &context);
}
}
const CONFLICT_OFFSET: i32 = 0x200;
fn l2_conflict() -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin(vec![
i().ld(T2, A1, CONFLICT_OFFSET).build(),
i().lui(T1, 1).build(),
i().add(T1, A1, T1).build(),
i().add(T1, T1, T2).build(),
i().ld(T2, T1, CONFLICT_OFFSET).build(),
])
}
fn an_inclusive_l2_eviction_drops_the_l1_copy(rec: &mut Recorder) {
for backend in BACKENDS {
for (policy, dropped) in [(InclusionPolicy::Inclusive, 1), (InclusionPolicy::Nine, 0)] {
let context = format!("{backend:?} {policy:?}");
let mut config = hierarchy(backend);
sized(&mut config.cache.l1_d, 2048, 4);
sized(&mut config.cache.l2, 4096, 1);
config.cache.inclusion_policy = policy;
let ctx = run_settled(&config, l2_conflict(), &context);
rec.expect(&ctx.sim, "core0.cache.l1d.back_invalidations", dropped, &context);
rec.expect(&ctx.sim, "core0.cache.l2.evictions", 1, &context);
}
}
}
fn back_invalidation_of_a_dropped_line() -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin(vec![
i().ld(T2, A1, CONFLICT_OFFSET).build(),
i().add(A1, A1, T2).build(),
i().ld(T2, A1, CONFLICT_OFFSET + 1024).build(),
i().lui(T1, 1).build(),
i().add(T1, A1, T1).build(),
i().add(T1, T1, T2).build(),
i().ld(T2, T1, CONFLICT_OFFSET).build(),
])
}
fn an_inclusive_llc_eviction_drops_the_l2_copy(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let mut config = hierarchy(backend);
sized(&mut config.cache.l3, 4096, 1);
config.cache.inclusion_policy = InclusionPolicy::Inclusive;
let ctx = run_settled(&config, l2_conflict(), &context);
rec.expect(&ctx.sim, "llc.evictions", 1, &context);
rec.expect(&ctx.sim, "core0.cache.l2.back_invalidations", 1, &context);
}
}
fn a_back_invalidation_for_a_line_already_gone_drops_nothing(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let mut config = hierarchy(backend);
sized(&mut config.cache.l2, 4096, 1);
config.cache.inclusion_policy = InclusionPolicy::Inclusive;
let ctx = run_settled(&config, back_invalidation_of_a_dropped_line(), &context);
rec.expect(&ctx.sim, "core0.cache.l2.evictions", 1, &context);
rec.expect(&ctx.sim, "core0.cache.l1d.back_invalidations", 0, &context);
}
}
fn a_cbo_passes_through_and_counts_at_every_level(rec: &mut Recorder) {
let i = InstructionBuilder::new;
let cbo_clean =
((CBO_CLEAN_IMM as u32 & 0xFFF) << 20) | (A1 << 15) | (i_f3::CBO << 12) | i_op::OP_MISC_MEM;
let program = ending_in_spin(vec![
i().addi(T0, 0, 5).build(),
i().sd(A1, T0, 0).build(),
cbo_clean,
FENCE_IORW,
]);
for backend in BACKENDS {
let context = format!("{backend:?}");
let ctx = run_settled(&hierarchy(backend), program.clone(), &context);
for cache in ["core0.cache.l1d", "core0.cache.l2", "llc"] {
rec.expect(&ctx.sim, &format!("{cache}.maintenance"), 1, &context);
}
rec.expect(&ctx.sim, "core0.cache.l1i.maintenance", 0, &context);
}
}
fn independent_misses() -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin((0..4).map(|n| i().ld(T0 + n % 3, A1, 64 * n as i32).build()).collect())
}
fn misses_beyond_the_mshrs_wait_as_blocked_requests(rec: &mut Recorder) {
let context = "OutOfOrder width 4";
let mut blocked = [0; 2];
for (n, mshrs) in [2, 8].into_iter().enumerate() {
let mut config = hierarchy(BackendKind::OutOfOrder);
config.pipeline.width = 4;
config.cache.l1_d.mshr_count = mshrs;
let ctx = run_settled(&config, independent_misses(), context);
blocked[n] = rec.read(&ctx.sim, "core0.cache.l1d.blocked_requests");
}
assert!(blocked[0] > 0, "{context}: two MSHRs: {blocked:?}");
assert_eq!(blocked[1], 0, "{context}: eight MSHRs: {blocked:?}");
}
fn page_end_then_middle() -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin(vec![
i().lui(T1, 1).build(),
i().add(T1, A1, T1).build(),
i().ld(T2, T1, -64).build(),
i().add(A1, A1, T2).build(),
i().ld(T2, A1, 512).build(),
])
}
fn a_next_line_candidate_past_the_page_is_dropped(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let mut config = hierarchy(backend);
config.cache.l1_d.prefetcher = PrefetcherKind::NextLine;
let ctx = run_settled(&config, page_end_then_middle(), &context);
rec.expect(&ctx.sim, "core0.cache.l1d.prefetches.page_crossing", 1, &context);
rec.expect(&ctx.sim, "core0.cache.l1d.prefetches.issued", 1, &context);
}
}
fn store_run(lines: i32) -> (Vec<u32>, u64) {
let i = InstructionBuilder::new;
ending_in_spin((0..lines).map(|n| i().sd(A1, T0, 64 * n).build()).collect())
}
fn store_prefetches_go_to_the_l2_and_are_dropped_without_an_mshr(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let mut config = hierarchy(backend);
config.cache.store_prefetcher = StorePrefetcherConfig::Stream { streams: 4, l2_lines: 2 };
config.cache.l2.mshr_count = 16;
let mut starved = config.clone();
starved.cache.l2.mshr_count = 1;
let ctx = run_settled(&config, store_run(8), &context);
let starved = run_settled(&starved, store_run(8), &context);
let sent = rec.read(&ctx.sim, "core0.cache.l1d.prefetches.store_stream");
assert!(sent > 0, "{context}");
rec.expect(&ctx.sim, "core0.cache.l2.prefetches.dropped", 0, &context);
let started = rec.read(&ctx.sim, "core0.cache.l2.prefetches.issued");
assert!((1..=sent).contains(&started), "{context}: {started} of {sent} started");
let dropped = rec.read(&starved.sim, "core0.cache.l2.prefetches.dropped");
let starved_sent = rec.read(&starved.sim, "core0.cache.l1d.prefetches.store_stream");
assert_eq!(dropped, starved_sent, "{context}: one MSHR is never given to a prefetch");
}
}
fn everything_on(backend: BackendKind) -> Config {
let mut config = prefetching_hierarchy(backend);
config.cache.inclusion_policy = InclusionPolicy::Inclusive;
config.cache.load_prefetcher = crate::config::LoadPrefetcherConfig::Stride {
table_size: 64,
l1_lines: 2,
l2_lines: 4,
page_boundary: crate::config::PageBoundary::Stop,
};
config
}
fn stats_a_level_cannot_count_stay_zero(rec: &mut Recorder) {
let never = [
("core0.cache.l1i.writebacks", "nothing to write back"),
("core0.cache.l1i.prefetches.store_stream", "no store prefetcher"),
("core0.cache.l2.prefetches.store_stream", "no store prefetcher"),
("llc.prefetches.store_stream", "no store prefetcher"),
("core0.cache.l1i.prefetches.dropped", "no prefetch request reaches it"),
("llc.prefetches.dropped", "no prefetch request targets it"),
("core0.cache.l1i.blocked_requests", "one fetch in flight"),
("llc.back_invalidations", "no level below it"),
];
for backend in BACKENDS {
let context = format!("{backend:?}");
let ctx = run_settled(&everything_on(backend), read_modify_write_lines(512), &context);
for (path, why) in never {
rec.expect(&ctx.sim, path, 0, &format!("{context}: {why}"));
}
}
}
fn every_level_writes_back_and_drops_what_an_inclusive_level_below_evicts(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let ctx = run_settled(&everything_on(backend), read_modify_write_lines(512), &context);
for cache in ["core0.cache.l1d", "core0.cache.l2"] {
let written = rec.read(&ctx.sim, &format!("{cache}.writebacks"));
let evicted = rec.read(&ctx.sim, &format!("{cache}.evictions"));
assert!((1..=evicted).contains(&written), "{context}: {cache}: {written} of {evicted}");
}
let dropped = rec.read(&ctx.sim, "core0.cache.l1i.back_invalidations");
assert!(dropped > 0, "{context}: the code lines evicted from the L2");
}
}
fn misses_beyond_each_levels_mshrs_wait_there(rec: &mut Recorder) {
let i = InstructionBuilder::new;
let program =
ending_in_spin((0..8).map(|n| i().ld(T0 + n % 3, A1, 64 * n as i32).build()).collect());
let context = "OutOfOrder width 4";
let mut config = hierarchy(BackendKind::OutOfOrder);
config.pipeline.width = 4;
config.cache.l1_d.mshr_count = 8;
config.cache.l2.mshr_count = 2;
config.cache.l3.mshr_count = 1;
let ctx = run_settled(&config, program, context);
for cache in ["core0.cache.l2", "llc"] {
let blocked = rec.read(&ctx.sim, &format!("{cache}.blocked_requests"));
assert!(blocked > 0, "{context}: {cache}: more misses than MSHRs");
}
}
fn next_line_candidates_past_the_page_are_dropped_at_every_level(rec: &mut Recorder) {
let i = InstructionBuilder::new;
let page_end = super::program::PROGRAM_BASE + 4096 - 4;
let program = vec![
i().lui(T1, 1).build(),
i().add(T1, A1, T1).build(),
i().ld(T2, T1, -64).build(),
i().add(T2, T2, T2).build(),
i().lui(T0, 1).build(),
i().auipc(T1, 0).build(),
i().add(T1, T1, T0).build(),
i().jalr(0, T1, -4 - 20).build(),
];
for backend in BACKENDS {
let context = format!("{backend:?}");
let mut config = hierarchy(backend);
for cache in [&mut config.cache.l1_i, &mut config.cache.l2, &mut config.cache.l3] {
cache.prefetcher = PrefetcherKind::NextLine;
}
let mut ctx = system_with(&config, &program, &[]);
super::program::store_words(&mut ctx, page_end, &[i().jal(0, 0).build()]);
run_to_pc(&mut ctx, page_end, &context);
ctx.run(SETTLE);
for cache in ["core0.cache.l1i", "core0.cache.l2", "llc"] {
let crossing = rec.read(&ctx.sim, &format!("{cache}.prefetches.page_crossing"));
assert!(crossing > 0, "{context}: {cache}");
}
}
}
fn load_prefetches_the_l1d_cannot_take_are_dropped(rec: &mut Recorder) {
for backend in BACKENDS {
let context = format!("{backend:?}");
let mut dropped = [0; 2];
for (n, mshrs) in [2, 16].into_iter().enumerate() {
let mut config = everything_on(backend);
config.cache.l1_d.prefetcher = PrefetcherKind::None;
config.cache.l1_d.mshr_count = mshrs;
let ctx = run_settled(&config, read_modify_write_lines(48), &context);
dropped[n] = rec.read(&ctx.sim, "core0.cache.l1d.prefetches.dropped");
}
assert!(dropped[0] > dropped[1], "{context}: two MSHRs against sixteen: {dropped:?}");
}
}
accounting_checks!(
every_level_fills_each_fetch_once_and_holds_what_it_did_not_evict,
a_cold_line_misses_once_and_then_hits,
a_dirty_victim_is_evicted_and_written_back,
an_inclusive_l2_eviction_drops_the_l1_copy,
a_back_invalidation_for_a_line_already_gone_drops_nothing,
an_inclusive_llc_eviction_drops_the_l2_copy,
a_cbo_passes_through_and_counts_at_every_level,
misses_beyond_the_mshrs_wait_as_blocked_requests,
a_next_line_candidate_past_the_page_is_dropped,
store_prefetches_go_to_the_l2_and_are_dropped_without_an_mshr,
stats_a_level_cannot_count_stay_zero,
every_level_writes_back_and_drops_what_an_inclusive_level_below_evicts,
misses_beyond_each_levels_mshrs_wait_there,
next_line_candidates_past_the_page_are_dropped_at_every_level,
load_prefetches_the_l1d_cannot_take_are_dropped,
);