Skip to content

Commit

Permalink
Feature/benchmarks and memory optimizations (#103)
Browse files Browse the repository at this point in the history
* Add benchmark program

* Refactor ProcessorStatus from class to struct to reduce
object memory allocation.

* Refactor instrumentation to reduce object creation
and increase performance.
Make instrumentation optional and only activated
when stats are shown in UI.

* Refactor sprite collision code to not allocate objects on heap.
  • Loading branch information
highbyte authored Jun 20, 2024
1 parent fe9dc72 commit aca5a98
Show file tree
Hide file tree
Showing 71 changed files with 1,306 additions and 357 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,5 @@ nuget.config

.DS_Store
.vscode/settings.json

BenchmarkDotNet.Artifacts/
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;

namespace Highbyte.DotNet6502.Benchmarks;

[MemoryDiagnoser] // Memory diagnoser is used to measure memory allocations
//[ShortRunJob] // WARNING: ShortRunJob is a custom job runs faster than normal, but is less accurate.
//[DryJob] // DANGER: DryJob is a custom job that runs very quickly, but VERY INACCURATE. Use only to verify that benchmarks actual runs.
public class Execute6502InstructionBenchmark
{
private CPU _cpu = default!;
private Memory _mem = default!;
private ushort _startAddress;

[Params(1)]
//[Params(1, 10, 100)]
public int NumberOfInstructionsToExecute;

// GlobalSetup is executed once, or if Params are used: once per each Params value combination
[GlobalSetup]
public void Setup()
{
Console.WriteLine("// " + "GlobalSetup");

_cpu = new CPU();
_mem = new Memory();
_startAddress = 0xc000;
LoadProgram(_mem, _startAddress);

}

private void LoadProgram(Memory mem, ushort startAddress)
{
var bytes = new byte[64 * 1024];
//// Random code (could include illegal opcodes)
////new Random(42).NextBytes(bytes);
//for (int i = 0; i < bytes.Length; i++)
//{
// bytes[i] = (byte)OpCodeId.NOP;
//}
//mem.StoreData(0, bytes);

//JSR + LDA_I + RTS + LDA_I + JMP-> 5 instructions
// Code at start address
ushort branchAddress = (ushort)(_startAddress + 0x10);
var address = _startAddress;
mem.WriteByte(ref address, OpCodeId.JSR);
mem.WriteWord(ref address, branchAddress);
mem.WriteByte(ref address, OpCodeId.LDA_I);
mem.WriteByte(ref address, 42);
mem.WriteByte(ref address, OpCodeId.JMP_ABS);
mem.WriteWord(ref address, _startAddress); // Jump back to start address

// Code at branch jsr address
address = branchAddress;
mem.WriteByte(ref address, OpCodeId.LDA_I);
mem.WriteByte(ref address, 21);
mem.WriteByte(ref address, OpCodeId.RTS);

}

//[GlobalCleanup]
//public void GlobalCleanup()
//{
// Console.WriteLine("// " + "GlobalCleanup");
//}

[Benchmark]
public void ExecIns()
{
_cpu.PC = _startAddress;
for (int i = 0; i < NumberOfInstructionsToExecute; i++)
{
_cpu.ExecuteOneInstruction(_mem);
}
}

[Benchmark]
public void ExecInsMinimal()
{
_cpu.PC = _startAddress;
for (int i = 0; i < NumberOfInstructionsToExecute; i++)
{
_cpu.ExecuteOneInstructionMinimal(_mem);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;

namespace Highbyte.DotNet6502.Benchmarks;

[MemoryDiagnoser] // Memory diagnoser is used to measure memory allocations
//[ShortRunJob] // WARNING: ShortRunJob is a custom job runs faster than normal, but is less accurate.
//[DryJob] // DANGER: DryJob is a custom job that runs very quickly, but VERY INACCURATE. Use only to verify that benchmarks actual runs.
public class ExecuteAll6502InstructionsBenchmark
{
private CPU _cpu = default!;
private Memory _mem = default!;
private ushort _startAddress;

[ParamsSource(nameof(OpCodes))]
public OpCodeId OpCode;

public IEnumerable<OpCodeId> OpCodes => Enum.GetValues<OpCodeId>();

// GlobalSetup is executed once, or if Params are used: once per each Params value combination
[GlobalSetup]
public void Setup()
{
Console.WriteLine("// " + "GlobalSetup");

_cpu = new CPU();
_mem = new Memory();
_startAddress = 0xc000;
}

//[GlobalCleanup]
//public void GlobalCleanup()
//{
// Console.WriteLine("// " + "GlobalCleanup");
//}

//[Benchmark]
//public void ExecIns()
//{
// _cpu.PC = _startAddress;
// _mem.WriteByte(_startAddress, (byte)OpCode);
// _cpu.ExecuteOneInstruction(_mem);
//}

[Benchmark]
public void ExecInsMinimal()
{
_cpu.PC = _startAddress;
_mem.WriteByte(_startAddress, (byte)OpCode);
_cpu.ExecuteOneInstructionMinimal(_mem);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;
using Highbyte.DotNet6502.Systems;
using Highbyte.DotNet6502.Systems.Commodore64;
using Highbyte.DotNet6502.Systems.Commodore64.Config;
using Microsoft.Extensions.Logging.Abstractions;

namespace Highbyte.DotNet6502.Benchmarks.Commodore64;

[MemoryDiagnoser] // Memory diagnoser is used to measure memory allocations
[ShortRunJob] // WARNING: ShortRunJob is a custom job runs faster than normal, but is less accurate.
//[DryJob] // DANGER: DryJob is a custom job that runs very quickly, but VERY INACCURATE. Use only to verify that benchmarks actual runs.
public class C64ExecuteFrameBenchmark
{
private C64 _c64WithInstrumentation = default!;
private C64 _c64WithoutInstrumentation = default!;
private SystemRunner _systemRunnerWithInstrumentation = default!;
private SystemRunner _systemRunnerWithoutInstrumentation = default!;
private ushort _startAddress;

[Params(1)]
//[Params(1, 100, 1000)]
public int NumberOfFramesToExecute;

// GlobalSetup is executed once, or if Params are used: once per each Params value combination
[GlobalSetup]
public void Setup()
{
Console.WriteLine("// " + "GlobalSetup");

var c64ConfigWithInstrumentation = new C64Config
{
C64Model = "C64NTSC", // C64NTSC, C64PAL
Vic2Model = "NTSC", // NTSC, NTSC_old, PAL
LoadROMs = false,
TimerMode = TimerMode.UpdateEachRasterLine,
AudioEnabled = true,
InstrumentationEnabled = true
};
_c64WithInstrumentation = C64.BuildC64(c64ConfigWithInstrumentation, new NullLoggerFactory());

c64ConfigWithInstrumentation.InstrumentationEnabled = false;
_c64WithoutInstrumentation = C64.BuildC64(c64ConfigWithInstrumentation, new NullLoggerFactory());

_startAddress = 0xc000;

LoadProgram(_c64WithInstrumentation.Mem, _startAddress);
LoadProgram(_c64WithoutInstrumentation.Mem, _startAddress);

var systemRunnerBuilderWithInstrumentation = new SystemRunnerBuilder<C64, NullRenderContext, NullInputHandlerContext, NullAudioHandlerContext>(_c64WithInstrumentation);
_systemRunnerWithInstrumentation = systemRunnerBuilderWithInstrumentation.Build();

var systemRunnerBuilderWithoutInstrumentation = new SystemRunnerBuilder<C64, NullRenderContext, NullInputHandlerContext, NullAudioHandlerContext>(_c64WithoutInstrumentation);
_systemRunnerWithoutInstrumentation = systemRunnerBuilderWithoutInstrumentation.Build();

}

private void LoadProgram(Memory mem, ushort startAddress)
{
var bytes = new byte[64 * 1024];
// Random code (could include illegal opcodes)
//new Random(42).NextBytes(bytes);
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = (byte)OpCodeId.RTS;
}
mem.StoreData(0, bytes);

// JSR + LDA_I + RTS + LDA_I + JMP -> 5 instructions
// Code at start address
//ushort branchAddress = (ushort)(_startAddress + 0x10);
//var address = _startAddress;
//mem.WriteByte(ref address, OpCodeId.JSR);
//mem.WriteWord(ref address, branchAddress);
//mem.WriteByte(ref address, OpCodeId.LDA_I);
//mem.WriteByte(ref address, 42);
//mem.WriteByte(ref address, OpCodeId.JMP_ABS);
//mem.WriteWord(ref address, _startAddress); // Jump back to start address

//// Code at branch jsr address
//address = branchAddress;
//mem.WriteByte(ref address, OpCodeId.LDA_I);
//mem.WriteByte(ref address, 21);
//mem.WriteByte(ref address, OpCodeId.RTS);

}

//[GlobalCleanup]
//public void GlobalCleanup()
//{
// Console.WriteLine("// " + "GlobalCleanup");
//}

[Benchmark(Baseline = true)]
public void ExecFrameWithoutInstrumentation()
{
_c64WithoutInstrumentation.CPU.PC = _startAddress;
for (var i = 0; i < NumberOfFramesToExecute; i++)
{
var execEvaluatorTriggerResult = _c64WithoutInstrumentation.ExecuteOneFrame(_systemRunnerWithoutInstrumentation);
}
}

[Benchmark]
public void ExecFrameWithInstrumentation()
{
_c64WithInstrumentation.CPU.PC = _startAddress;
for (var i = 0; i < NumberOfFramesToExecute; i++)
{
var execEvaluatorTriggerResult = _c64WithInstrumentation.ExecuteOneFrame(_systemRunnerWithInstrumentation);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Jobs;
using Highbyte.DotNet6502.Systems;
using Highbyte.DotNet6502.Systems.Commodore64;
using Highbyte.DotNet6502.Systems.Commodore64.Config;
using Microsoft.Extensions.Logging.Abstractions;

namespace Highbyte.DotNet6502.Benchmarks.Commodore64;

[MemoryDiagnoser] // Memory diagnoser is used to measure memory allocations
//[ShortRunJob] // WARNING: ShortRunJob is a custom job runs faster than normal, but is less accurate.
//[DryJob] // DANGER: DryJob is a custom job that runs very quickly, but VERY INACCURATE. Use only to verify that benchmarks actual runs.
public class C64ExecuteInstructionBenchmark
{
private C64 _c64WithInstrumentation = default!;
private C64 _c64WithoutInstrumentation = default!;
private SystemRunner _systemRunnerWithInstrumentation = default!;
private SystemRunner _systemRunnerWithoutInstrumentation = default!;
private ushort _startAddress;

//[Params(1)]
[Params(1, 100, 1000)]
public int NumberOfInstructionsToExecute;

// GlobalSetup is executed once, or if Params are used: once per each Params value combination
[GlobalSetup]
public void Setup()
{
Console.WriteLine("// " + "GlobalSetup");

var c64ConfigWithInstrumentation = new C64Config
{
C64Model = "C64NTSC", // C64NTSC, C64PAL
Vic2Model = "NTSC", // NTSC, NTSC_old, PAL
LoadROMs = false,
TimerMode = TimerMode.UpdateEachRasterLine,
AudioEnabled = true,
InstrumentationEnabled = true
};
_c64WithInstrumentation = C64.BuildC64(c64ConfigWithInstrumentation, new NullLoggerFactory());

c64ConfigWithInstrumentation.InstrumentationEnabled = false;
_c64WithoutInstrumentation = C64.BuildC64(c64ConfigWithInstrumentation, new NullLoggerFactory());

_startAddress = 0xc000;

LoadProgram(_c64WithInstrumentation.Mem, _startAddress);
LoadProgram(_c64WithoutInstrumentation.Mem, _startAddress);

var systemRunnerBuilderWithInstrumentation = new SystemRunnerBuilder<C64, NullRenderContext, NullInputHandlerContext, NullAudioHandlerContext>(_c64WithInstrumentation);
_systemRunnerWithInstrumentation = systemRunnerBuilderWithInstrumentation.Build();

var systemRunnerBuilderWithoutInstrumentation = new SystemRunnerBuilder<C64, NullRenderContext, NullInputHandlerContext, NullAudioHandlerContext>(_c64WithoutInstrumentation);
_systemRunnerWithoutInstrumentation = systemRunnerBuilderWithoutInstrumentation.Build();

}

private void LoadProgram(Memory mem, ushort startAddress)
{
var bytes = new byte[64 * 1024];
// Random code (could include illegal opcodes)
//new Random(42).NextBytes(bytes);
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = (byte)OpCodeId.RTS;
}
mem.StoreData(0, bytes);

// JSR + LDA_I + RTS + LDA_I + JMP -> 5 instructions
// Code at start address
//ushort branchAddress = (ushort)(_startAddress + 0x10);
//var address = _startAddress;
//mem.WriteByte(ref address, OpCodeId.JSR);
//mem.WriteWord(ref address, branchAddress);
//mem.WriteByte(ref address, OpCodeId.LDA_I);
//mem.WriteByte(ref address, 42);
//mem.WriteByte(ref address, OpCodeId.JMP_ABS);
//mem.WriteWord(ref address, _startAddress); // Jump back to start address

//// Code at branch jsr address
//address = branchAddress;
//mem.WriteByte(ref address, OpCodeId.LDA_I);
//mem.WriteByte(ref address, 21);
//mem.WriteByte(ref address, OpCodeId.RTS);

}

//[GlobalCleanup]
//public void GlobalCleanup()
//{
// Console.WriteLine("// " + "GlobalCleanup");
//}

[Benchmark(Baseline = true)]
public void ExecInsWithoutInstrumentation()
{
_c64WithoutInstrumentation.CPU.PC = _startAddress;
for (var i = 0; i < NumberOfInstructionsToExecute; i++)
{
_c64WithoutInstrumentation.ExecuteOneInstruction(_systemRunnerWithoutInstrumentation, out InstructionExecResult instructionExecResult);
}
}

[Benchmark]
public void ExecInsWithInstrumentation()
{
_c64WithInstrumentation.CPU.PC = _startAddress;
for (var i = 0; i < NumberOfInstructionsToExecute; i++)
{
_c64WithInstrumentation.ExecuteOneInstruction(_systemRunnerWithInstrumentation, out InstructionExecResult instructionExecResult);
}
}
}
Loading

0 comments on commit aca5a98

Please sign in to comment.