Skip to content

Commit

Permalink
perf(cpu): speed up interrupts
Browse files Browse the repository at this point in the history
code which makes heavy use of interrupts considerably slows down the simulator. E.g. that transmit programs  large amount of data over SPI.

See wokwi/wokwi-features#280 for an example.
  • Loading branch information
urish committed Jan 20, 2022
1 parent 8c1f76f commit c029ed1
Showing 1 changed file with 28 additions and 11 deletions.
39 changes: 28 additions & 11 deletions src/cpu/cpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { u32, u16, u8, i16 } from '../types';
import { avrInterrupt } from './interrupt';

const registerSpace = 0x100;
const MAX_INTERRUPTS = 128; // Enough for ATMega2560

export type CPUMemoryHook = (value: u8, oldValue: u8, addr: u16, mask: u8) => boolean | void;
export interface CPUMemoryHooks {
Expand Down Expand Up @@ -46,7 +47,7 @@ export class CPU {
readonly progBytes = new Uint8Array(this.progMem.buffer);
readonly readHooks: CPUMemoryReadHooks = [];
readonly writeHooks: CPUMemoryHooks = [];
private readonly pendingInterrupts: AVRInterruptConfig[] = [];
private readonly pendingInterrupts: (AVRInterruptConfig | null)[] = new Array(MAX_INTERRUPTS);
private nextClockEvent: AVRClockEventEntry | null = null;
private readonly clockEventPool: AVRClockEventEntry[] = []; // helps avoid garbage collection

Expand Down Expand Up @@ -77,6 +78,7 @@ export class CPU {
cycles = 0;

nextInterrupt: i16 = -1;
maxInterrupt: i16 = 0;

constructor(public progMem: Uint16Array, private sramBytes = 8192) {
this.reset();
Expand All @@ -86,7 +88,7 @@ export class CPU {
this.data.fill(0);
this.SP = this.data.length - 1;
this.pc = 0;
this.pendingInterrupts.splice(0, this.pendingInterrupts.length);
this.pendingInterrupts.fill(null);
this.nextInterrupt = -1;
this.nextClockEvent = null;
}
Expand Down Expand Up @@ -124,10 +126,6 @@ export class CPU {
return this.SREG & 0x80 ? true : false;
}

private updateNextInterrupt() {
this.nextInterrupt = this.pendingInterrupts.findIndex((item) => !!item);
}

setInterruptFlag(interrupt: AVRInterruptConfig) {
const { flagRegister, flagMask, enableRegister, enableMask } = interrupt;
if (interrupt.inverseFlag) {
Expand All @@ -153,16 +151,34 @@ export class CPU {
}

queueInterrupt(interrupt: AVRInterruptConfig) {
this.pendingInterrupts[interrupt.address] = interrupt;
this.updateNextInterrupt();
const { address } = interrupt;
this.pendingInterrupts[address] = interrupt;
if (this.nextInterrupt === -1 || this.nextInterrupt > address) {
this.nextInterrupt = address;
}
if (address > this.maxInterrupt) {
this.maxInterrupt = address;
}
}

clearInterrupt({ address, flagRegister, flagMask }: AVRInterruptConfig, clearFlag = true) {
delete this.pendingInterrupts[address];
if (clearFlag) {
this.data[flagRegister] &= ~flagMask;
}
this.updateNextInterrupt();
const { pendingInterrupts, maxInterrupt } = this;
if (!pendingInterrupts[address]) {
return;
}
pendingInterrupts[address] = null;
if (this.nextInterrupt === address) {
this.nextInterrupt = -1;
for (let i = address + 1; i <= maxInterrupt; i++) {
if (pendingInterrupts[i]) {
this.nextInterrupt = i;
break;
}
}
}
}

clearInterruptByFlag(interrupt: AVRInterruptConfig, registerValue: number) {
Expand Down Expand Up @@ -241,7 +257,8 @@ export class CPU {

const { nextInterrupt } = this;
if (this.interruptsEnabled && nextInterrupt >= 0) {
const interrupt = this.pendingInterrupts[nextInterrupt];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const interrupt = this.pendingInterrupts[nextInterrupt]!;
avrInterrupt(this, interrupt.address);
if (!interrupt.constant) {
this.clearInterrupt(interrupt);
Expand Down

3 comments on commit c029ed1

@sutaburosu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! I fear JS/TS will always be a foreign language to me, but I can appreciate that you've made the browser's job much easier by replacing many null comparisons with far fewer. 👍

@urish
Copy link
Contributor Author

@urish urish commented on c029ed1 Jan 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I planned to make a video where I show how I profile this and find the bottleneck. But I figured out it'll be too much distraction to edit it, promote it, etc. so I ditched that plan.

In general, the offending line was:

    delete this.pendingInterrupts[address];

I'm not really sure why it was that expensive - my bet is that it converts the array into sparse array, possibly allocating new memory and copying over all the content.

We had a similar win in 968a6ee, where I switched from an array to a more performant linked list.

@sutaburosu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for expanding on that. Avoiding the sparse -> full allocation would certainly help too, especially in a garbage-collected language. JS makes slightly more sense to me each time you describe something, so maybe one day I won't be such a complete stranger to it. ❤️

Please sign in to comment.