Note: I base this answer on the assumption that you're writing an emulation of a real hardware device. I assume this is true largely because of its suspicious number of instructions and the mention of addressing modes.
Is switch statement the best choice here and what other optimization can I do to make sure the simulation runs fluently?
The best way to make sure your system runs fluently is to test it and see if it meets your predefined criteria for "fluently". A good benchmark here is the advertised speed of the hardware you're trying to emulate. If I understand correctly and that speed is 1.78MHz, then if you have a 1.78GHz processor (which is approximately smartphone speed in late 2019), you just need to execute one emulated instruction with 1000 or less of your own instructions.
Let's assume that your code fails your benchmark for "fluent". The next step is to profile your code and see where you're actually spending too much time. It may or may not be in your switch statement.
Let's assume the switch statement is the problem. Modern compilers are really good at optimizations. If you have a switch statement like this, I'd expect it to already be using something like a Branch Table. It may be worth checking your compiler's generated assembly for this kind of technique before you implement it yourself. Compiler writers are very smart; if the compiler did it already, it's unlikely that you will do better by hand.
Let's assume the attempts at optimizing the switch statement failed. The next thing I'd recommend is looking into how your target CPU works. See if it uses microcode. If it's a real CPU, then it's nearly certain they were designed with hardware engineering input. Try some analysis on the instruction sets. 256 instructions is just eight bits, so look for patterns in the bit representations of related instructions. For example, if there is an "add from register" and "add immediate value", they likely vary by one bit. Look for similar multiply and subtract instructions. There is a good chance that you can deduce something like "bit 3 controls whether the first argument is an immediate value or a register number".
Let's assume that you found some patterns. Consider emulating multiple instructions at once in a phased approach. Assuming you have a five bit instruction that always takes three arguments, you may get something like this:
// Parse out these values in advance
int instruction_id = memory[program_counter];
int firstArgRaw = memory[program_counter + 1];
int secondArgRaw = memory[program_counter + 2];
int thirdArgRaw = memory[program_counter + 3];
// Resolve first argument based on bit 0
int firstArgValue = 0;
if (instruction_id & FIRST_ARG_BITMASK){
firstArgValue = firstArgRaw;
} else {
firstArgValue = registers[firstArgRaw];
}
// Resolve second argument based on bit 1
int secondArgValue = 0;
if (instruction_id & SECOND_ARG_BITMASK){
secondArgValue = secondArgRaw;
} else {
secondArgValue = registers[secondArgRaw];
}
// Apply operator to args 1 and 2, which is defined by bits 2 and 3
int result = 0;
if (instruction_id & ADD_BITMASK == ADD_SUBINSTRUCTION){
result = firstArgValue + secondArgValue;
} else if (instruction_id & SUB_BITMASK == SUB_SUBINSTRUCTION){
result = firstArgValue - secondArgValue;
} else if (instruction_id & MUL_BITMASK == MUL_SUBINSTRUCTION){
result = firstArgValue * secondArgValue;
} else if (instruction_id & DIV_BITMASK == DIV_SUBINSTRUCTION){
result = firstArgValue / secondArgValue;
}
// Save result to register or memory as defined by bit 4
if (instruction_id & RESULT_BITMASK) {
memory[thirdArgRaw] = result;
} else {
registers[thirdArgRaw] = result;
}
The above is around 31 lines of code and handles 32 different instructions. Your results may vary.
It would be pretty natural in this setup to do situational checks for specific values early on to bypass most logic. Noop comes to mind here.
Let's suppose that helped, but not quite enough. Naturally, you should profile to see what's slow. One potential issue is the above code involves a bunch of if statements. Since the data is essentially random, this can be very hard on your CPU's branch predictor. If the mathematics works out, you might be able to optimize an if statement like this
int firstArgValue = 0;
if (instruction_id & FIRST_ARG_BITMASK){
firstArgValue = firstArgRaw;
} else {
firstArgValue = registers[firstArgRaw];
}
into something like this
int firstArgValue = (instruction_id & FIRST_ARG_BITMASK) * firstArgRaw +
!(instruction_id & FIRST_ARG_BITMASK) * registers[firstArgRaw];
The idea here is to calculate both values, multiply one of them by 1 (to keep it) and the other by 0 (to discard it), then add them together. You will always do this and get the right value; there is no branch here to mispredict. A branch and one or two instructions may well be slower than a fixed five instructions if it invalidates your instruction pipelining. As with all performance tweaks, you don't know whether it helped or hurt until you've measured it. In this case, it's also a bit obfuscated, so a comment clarifying why you didn't use the more obvious if statement is in order.