Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions benchmarks/accuracy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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, "", " ")
Expand Down
8 changes: 8 additions & 0 deletions benchmarks/cpi_comparison_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
4 changes: 4 additions & 0 deletions benchmarks/dcache_accuracy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions benchmarks/timing_harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions benchmarks/timing_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
60 changes: 48 additions & 12 deletions emu/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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[:])
}
Expand All @@ -39,15 +75,15 @@ 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])
}
}

// Read32 reads a 32-bit little-endian value from memory.
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[:])
}
Expand All @@ -57,15 +93,15 @@ 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])
}
}

// Read64 reads a 64-bit little-endian value from memory.
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[:])
}
Expand All @@ -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)
}
}
81 changes: 57 additions & 24 deletions timing/pipeline/stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,62 @@
// 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

Check failure on line 35 in timing/pipeline/stages.go

View workflow job for this annotation

GitHub Actions / Lint

File is not properly formatted (gofmt)
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.
Expand Down Expand Up @@ -94,23 +142,18 @@

// 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.
Expand All @@ -120,28 +163,18 @@
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.
Expand Down
Loading