diff --git a/benchmarks/accuracy_test.go b/benchmarks/accuracy_test.go index 4e37ac9..a5d26be 100644 --- a/benchmarks/accuracy_test.go +++ b/benchmarks/accuracy_test.go @@ -137,6 +137,10 @@ func calculateError(simCPI, baselineCPI float64) float64 { // TestAccuracyAgainstBaseline compares simulator results against M2 baseline. // This is the main accuracy validation test for M2Sim. func TestAccuracyAgainstBaseline(t *testing.T) { + if testing.Short() { + t.Skip("Skipping long-running accuracy test in short mode") + } + // Load baseline data baseline := loadBaseline(t) @@ -212,6 +216,10 @@ func TestAccuracyAgainstBaseline(t *testing.T) { // TestAccuracyDependencyChain specifically tests the dependency chain accuracy. // This is critical because it measures the simulator's handling of RAW hazards. func TestAccuracyDependencyChain(t *testing.T) { + if testing.Short() { + t.Skip("Skipping accuracy test in short mode") + } + baseline := loadBaseline(t) baselineEntry := findBaseline(baseline, "dependency") if baselineEntry == nil { @@ -249,6 +257,10 @@ func TestAccuracyDependencyChain(t *testing.T) { // TestAccuracyArithmetic tests ALU throughput accuracy. func TestAccuracyArithmetic(t *testing.T) { + if testing.Short() { + t.Skip("Skipping accuracy test in short mode") + } + baseline := loadBaseline(t) baselineEntry := findBaseline(baseline, "arithmetic") if baselineEntry == nil { @@ -280,6 +292,10 @@ func TestAccuracyArithmetic(t *testing.T) { // TestAccuracyBranch tests branch handling accuracy using conditional branches. // Uses branchTakenConditional to match native benchmark pattern (CMP + B.GE). func TestAccuracyBranch(t *testing.T) { + if testing.Short() { + t.Skip("Skipping accuracy test in short mode") + } + baseline := loadBaseline(t) baselineEntry := findBaseline(baseline, "branch") if baselineEntry == nil { @@ -373,6 +389,10 @@ func GenerateAccuracyReport(t *testing.T) AccuracyReport { // TestGenerateAccuracyReport tests the report generation and outputs JSON. func TestGenerateAccuracyReport(t *testing.T) { + if testing.Short() { + t.Skip("Skipping long-running accuracy report generation in short mode") + } + report := GenerateAccuracyReport(t) reportJSON, err := json.MarshalIndent(report, "", " ") diff --git a/benchmarks/cpi_comparison_test.go b/benchmarks/cpi_comparison_test.go index 386c366..ccbd443 100644 --- a/benchmarks/cpi_comparison_test.go +++ b/benchmarks/cpi_comparison_test.go @@ -52,6 +52,10 @@ func runFastTimingBenchmark(bench Benchmark) (cycles uint64, instructions uint64 // that fast timing CPI approximations are reasonable relative to the detailed // pipeline model. func TestCPIComparison_FastVsFullPipeline(t *testing.T) { + if testing.Short() { + t.Skip("Skipping CPI comparison test in short mode") + } + config := DefaultConfig() config.Output = &bytes.Buffer{} config.EnableICache = false @@ -137,6 +141,10 @@ func TestCPIComparison_FastVsFullPipeline(t *testing.T) { // TestCPIComparison_ThreeWay extends the comparison to include M2 hardware // baselines, providing a three-way view: hardware vs full pipeline vs fast timing. func TestCPIComparison_ThreeWay(t *testing.T) { + if testing.Short() { + t.Skip("Skipping three-way CPI comparison test in short mode") + } + // M2 hardware CPI baselines (from calibration_results.json, at 3.5 GHz) // CPI = latency_ns * frequency_GHz m2Baselines := map[string]float64{ diff --git a/benchmarks/dcache_accuracy_test.go b/benchmarks/dcache_accuracy_test.go index b6e1b64..7fedcf1 100644 --- a/benchmarks/dcache_accuracy_test.go +++ b/benchmarks/dcache_accuracy_test.go @@ -11,6 +11,10 @@ import ( // Output format: " benchmark_name: CPI=X.XXX" matching the parser in // accuracy_report.py. func TestAccuracyCPI_WithDCache(t *testing.T) { + if testing.Short() { + t.Skip("Skipping D-cache accuracy test in short mode") + } + config := DefaultConfig() config.Output = &bytes.Buffer{} config.EnableICache = false diff --git a/benchmarks/timing_harness_test.go b/benchmarks/timing_harness_test.go index f3894d3..1be1df5 100644 --- a/benchmarks/timing_harness_test.go +++ b/benchmarks/timing_harness_test.go @@ -8,6 +8,10 @@ import ( ) func TestHarnessRunsAllBenchmarks(t *testing.T) { + if testing.Short() { + t.Skip("Skipping all benchmarks test in short mode") + } + config := DefaultConfig() config.Output = &bytes.Buffer{} config.Verbose = false diff --git a/benchmarks/timing_validation_test.go b/benchmarks/timing_validation_test.go index c391743..7c8d5cf 100644 --- a/benchmarks/timing_validation_test.go +++ b/benchmarks/timing_validation_test.go @@ -215,6 +215,10 @@ func TestTimingPredictions_FunctionCallOverhead(t *testing.T) { // TestTimingPredictions_CPIBounds validates that CPI is within reasonable bounds // for all benchmarks. func TestTimingPredictions_CPIBounds(t *testing.T) { + if testing.Short() { + t.Skip("Skipping CPI bounds test in short mode") + } + config := DefaultConfig() config.Output = &bytes.Buffer{} config.EnableICache = false @@ -325,6 +329,10 @@ func TestTimingPredictions_CacheEffect(t *testing.T) { // TestTimingPredictions_StallAccounting validates that total stalls // equal the sum of stall types. func TestTimingPredictions_StallAccounting(t *testing.T) { + if testing.Short() { + t.Skip("Skipping stall accounting test in short mode") + } + config := DefaultConfig() config.Output = &bytes.Buffer{} config.EnableICache = false @@ -355,6 +363,10 @@ func TestTimingPredictions_StallAccounting(t *testing.T) { // With 8-wide superscalar, we can retire up to 8 instructions per cycle, // so Cycles >= Instructions/8 (theoretically). func TestTimingPredictions_CycleEquation(t *testing.T) { + if testing.Short() { + t.Skip("Skipping cycle equation test in short mode") + } + config := DefaultConfig() config.Output = &bytes.Buffer{} config.EnableICache = false diff --git a/emu/memory.go b/emu/memory.go index 3943d6b..d4a21b3 100644 --- a/emu/memory.go +++ b/emu/memory.go @@ -3,33 +3,69 @@ package emu import "encoding/binary" -// Memory provides a simple byte-addressable memory model for emulation. +const ( + // pageSize defines the size of each memory page (64KB) + pageSize = 64 * 1024 + // pageMask is used to extract the page offset from an address + pageMask = pageSize - 1 +) + +// memoryPage represents a single page of memory +type memoryPage struct { + data [pageSize]byte +} + +// Memory provides a page-based byte-addressable memory model for emulation. +// This optimization replaces the map[uint64]byte approach with a page-based system +// to eliminate map access overhead, targeting the 3.95% CPU usage in Memory.Read32. type Memory struct { - data map[uint64]byte + pages map[uint64]*memoryPage } // NewMemory creates a new memory instance. func NewMemory() *Memory { return &Memory{ - data: make(map[uint64]byte), + pages: make(map[uint64]*memoryPage), } } +// getOrCreatePage gets an existing page or creates a new one for the given address. +func (m *Memory) getOrCreatePage(addr uint64) *memoryPage { + pageAddr := addr &^ pageMask + page, exists := m.pages[pageAddr] + if !exists { + page = &memoryPage{} + m.pages[pageAddr] = page + } + return page +} + +// getPage gets an existing page for the given address, or nil if it doesn't exist. +func (m *Memory) getPage(addr uint64) *memoryPage { + pageAddr := addr &^ pageMask + return m.pages[pageAddr] +} + // Read8 reads a single byte from memory. func (m *Memory) Read8(addr uint64) byte { - return m.data[addr] + page := m.getPage(addr) + if page == nil { + return 0 // Return 0 for uninitialized memory + } + return page.data[addr&pageMask] } // Write8 writes a single byte to memory. func (m *Memory) Write8(addr uint64, value byte) { - m.data[addr] = value + page := m.getOrCreatePage(addr) + page.data[addr&pageMask] = value } // Read16 reads a 16-bit little-endian value from memory. func (m *Memory) Read16(addr uint64) uint16 { var buf [2]byte for i := uint64(0); i < 2; i++ { - buf[i] = m.data[addr+i] + buf[i] = m.Read8(addr + i) } return binary.LittleEndian.Uint16(buf[:]) } @@ -39,7 +75,7 @@ func (m *Memory) Write16(addr uint64, value uint16) { var buf [2]byte binary.LittleEndian.PutUint16(buf[:], value) for i := uint64(0); i < 2; i++ { - m.data[addr+i] = buf[i] + m.Write8(addr+i, buf[i]) } } @@ -47,7 +83,7 @@ func (m *Memory) Write16(addr uint64, value uint16) { func (m *Memory) Read32(addr uint64) uint32 { var buf [4]byte for i := uint64(0); i < 4; i++ { - buf[i] = m.data[addr+i] + buf[i] = m.Read8(addr + i) } return binary.LittleEndian.Uint32(buf[:]) } @@ -57,7 +93,7 @@ func (m *Memory) Write32(addr uint64, value uint32) { var buf [4]byte binary.LittleEndian.PutUint32(buf[:], value) for i := uint64(0); i < 4; i++ { - m.data[addr+i] = buf[i] + m.Write8(addr+i, buf[i]) } } @@ -65,7 +101,7 @@ func (m *Memory) Write32(addr uint64, value uint32) { func (m *Memory) Read64(addr uint64) uint64 { var buf [8]byte for i := uint64(0); i < 8; i++ { - buf[i] = m.data[addr+i] + buf[i] = m.Read8(addr + i) } return binary.LittleEndian.Uint64(buf[:]) } @@ -75,13 +111,13 @@ func (m *Memory) Write64(addr uint64, value uint64) { var buf [8]byte binary.LittleEndian.PutUint64(buf[:], value) for i := uint64(0); i < 8; i++ { - m.data[addr+i] = buf[i] + m.Write8(addr+i, buf[i]) } } // LoadProgram loads a binary program into memory at the specified address. func (m *Memory) LoadProgram(addr uint64, program []byte) { for i, b := range program { - m.data[addr+uint64(i)] = b + m.Write8(addr+uint64(i), b) } } diff --git a/timing/pipeline/stages.go b/timing/pipeline/stages.go index 76b144d..3006177 100644 --- a/timing/pipeline/stages.go +++ b/timing/pipeline/stages.go @@ -30,14 +30,62 @@ type DecodeStage struct { // Supports up to 8 concurrent decode operations (for 8-wide superscalar pipelines) instPool [8]insts.Instruction poolIndex int + // Pre-computed opcode lookup tables for O(1) instruction classification + // Eliminates switch statement overhead that causes map access patterns (9.29% CPU usage) + isLoadOpTable [256]bool + isStoreOpTable [256]bool + isRegWriteOpTable [256]bool + isBranchOpTable [256]bool } // NewDecodeStage creates a new decode stage. func NewDecodeStage(regFile *emu.RegFile) *DecodeStage { - return &DecodeStage{ + stage := &DecodeStage{ regFile: regFile, decoder: insts.NewDecoder(), } + stage.initOpcodeLookupTables() + return stage +} + +// initOpcodeLookupTables initializes the pre-computed opcode classification tables. +func (s *DecodeStage) initOpcodeLookupTables() { + // Initialize load operation lookup table + loadOps := []insts.Op{ + insts.OpLDR, insts.OpLDP, insts.OpLDRB, insts.OpLDRSB, + insts.OpLDRH, insts.OpLDRSH, insts.OpLDRLit, insts.OpLDRQ, + } + for _, op := range loadOps { + s.isLoadOpTable[op] = true + } + + // Initialize store operation lookup table + storeOps := []insts.Op{ + insts.OpSTR, insts.OpSTP, insts.OpSTRB, insts.OpSTRH, insts.OpSTRQ, + } + for _, op := range storeOps { + s.isStoreOpTable[op] = true + } + + // Initialize register write operation lookup table + regWriteOps := []insts.Op{ + insts.OpADD, insts.OpSUB, insts.OpAND, insts.OpORR, insts.OpEOR, + insts.OpBIC, insts.OpORN, insts.OpEON, + insts.OpLDR, insts.OpLDP, insts.OpLDRB, insts.OpLDRSB, + insts.OpLDRH, insts.OpLDRSH, insts.OpLDRLit, insts.OpLDRQ, + insts.OpBL, insts.OpBLR, + } + for _, op := range regWriteOps { + s.isRegWriteOpTable[op] = true + } + + // Initialize branch operation lookup table + branchOps := []insts.Op{ + insts.OpB, insts.OpBL, insts.OpBCond, insts.OpBR, insts.OpBLR, insts.OpRET, + } + for _, op := range branchOps { + s.isBranchOpTable[op] = true + } } // DecodeResult contains the output of the decode stage. @@ -94,23 +142,18 @@ func (s *DecodeStage) Decode(word uint32, pc uint64) DecodeResult { // isLoadOp returns true if the opcode is a load operation. func (s *DecodeStage) isLoadOp(op insts.Op) bool { - switch op { - case insts.OpLDR, insts.OpLDP, insts.OpLDRB, insts.OpLDRSB, - insts.OpLDRH, insts.OpLDRSH, insts.OpLDRLit, insts.OpLDRQ: - return true - default: + if int(op) >= len(s.isLoadOpTable) { return false } + return s.isLoadOpTable[op] } // isStoreOp returns true if the opcode is a store operation. func (s *DecodeStage) isStoreOp(op insts.Op) bool { - switch op { - case insts.OpSTR, insts.OpSTP, insts.OpSTRB, insts.OpSTRH, insts.OpSTRQ: - return true - default: + if int(op) >= len(s.isStoreOpTable) { return false } + return s.isStoreOpTable[op] } // isRegWriteInst determines if the instruction writes to a register. @@ -120,28 +163,18 @@ func (s *DecodeStage) isRegWriteInst(inst *insts.Instruction) bool { return false } - switch inst.Op { - case insts.OpADD, insts.OpSUB, insts.OpAND, insts.OpORR, insts.OpEOR, - insts.OpBIC, insts.OpORN, insts.OpEON: - return true - case insts.OpLDR, insts.OpLDP, insts.OpLDRB, insts.OpLDRSB, - insts.OpLDRH, insts.OpLDRSH, insts.OpLDRLit, insts.OpLDRQ: - return true - case insts.OpBL, insts.OpBLR: - return true // BL/BLR write to X30 - default: + if int(inst.Op) >= len(s.isRegWriteOpTable) { return false } + return s.isRegWriteOpTable[inst.Op] } // isBranchInst determines if the instruction is a branch. func (s *DecodeStage) isBranchInst(inst *insts.Instruction) bool { - switch inst.Op { - case insts.OpB, insts.OpBL, insts.OpBCond, insts.OpBR, insts.OpBLR, insts.OpRET: - return true - default: + if int(inst.Op) >= len(s.isBranchOpTable) { return false } + return s.isBranchOpTable[inst.Op] } // ExecuteStage performs ALU operations.