mirror of
https://github.com/sethm/symon.git
synced 2025-04-14 21:37:21 +00:00
Faster byte and word to hex string
I was alarmed to discover just how slow `String.format()` is for doing integer to hex conversions. Now that a trace window has been added to Symon, it became especially clear that I needed a more efficient way to handle it. I looked into using `Integer.toHexString()`, but I would have had to wrap it to do zero-padding, so I decided to just bite the bullet and do my own with a lookup table. The implementation in `HexUtil` is just as fast as `Integer.toHexString()`, but also zero-pads appropriately. Combined with Java's `+` String concatenation, it seems perfectly adequate. For yet better performance with the trace window, it would make a lot of sense to special-case stepping so that it just pops the top line off the log, and appends to the bottom, rather than re-stringifying the entire trace log each time. This will be a future enhancement.
This commit is contained in:
parent
bc3de80892
commit
807a43ce6f
src
main/java/com/loomcom/symon
test/java/com/loomcom/symon
@ -24,6 +24,7 @@
|
||||
package com.loomcom.symon;
|
||||
|
||||
import com.loomcom.symon.exceptions.MemoryAccessException;
|
||||
import com.loomcom.symon.util.HexUtil;
|
||||
|
||||
/**
|
||||
* This class provides a simulation of the MOS 6502 CPU's state machine.
|
||||
@ -1134,23 +1135,23 @@ public class Cpu implements InstructionTable {
|
||||
}
|
||||
|
||||
public String getAccumulatorStatus() {
|
||||
return String.format("$%02X", state.a);
|
||||
return "$" + HexUtil.byteToHex(state.a);
|
||||
}
|
||||
|
||||
public String getXRegisterStatus() {
|
||||
return String.format("$%02X", state.x);
|
||||
return "$" + HexUtil.byteToHex(state.x);
|
||||
}
|
||||
|
||||
public String getYRegisterStatus() {
|
||||
return String.format("$%02X", state.y);
|
||||
return "$" + HexUtil.byteToHex(state.y);
|
||||
}
|
||||
|
||||
public String getProgramCounterStatus() {
|
||||
return String.format("$%04X", state.pc);
|
||||
return "$" + HexUtil.wordToHex(state.pc);
|
||||
}
|
||||
|
||||
public String getStackPointerStatus() {
|
||||
return String.format("$%02X", state.sp);
|
||||
return "$" + HexUtil.byteToHex(state.sp);
|
||||
}
|
||||
|
||||
public int getProcessorStatus() {
|
||||
@ -1270,7 +1271,7 @@ public class Cpu implements InstructionTable {
|
||||
|
||||
|
||||
/**
|
||||
* A compact representation of CPU state;
|
||||
* A compact, struct-like representation of CPU state.
|
||||
*/
|
||||
public static class CpuState {
|
||||
/**
|
||||
@ -1301,6 +1302,7 @@ public class Cpu implements InstructionTable {
|
||||
public int[] args = new int[2];
|
||||
public int instSize;
|
||||
public boolean opTrap;
|
||||
|
||||
/* Status Flag Register bits */
|
||||
public boolean carryFlag;
|
||||
public boolean negativeFlag;
|
||||
@ -1319,7 +1321,8 @@ public class Cpu implements InstructionTable {
|
||||
/**
|
||||
* Snapshot a copy of the CpuState.
|
||||
*
|
||||
* (This is a copy constructor rather than an implementation of Clonable based on Josh Bloch's recommendation)
|
||||
* (This is a copy constructor rather than an implementation of <code>Clonable</code>
|
||||
* based on Josh Bloch's recommendation)
|
||||
*
|
||||
* @param s The CpuState to copy.
|
||||
*/
|
||||
@ -1346,19 +1349,21 @@ public class Cpu implements InstructionTable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representing the CPU state.
|
||||
* Returns a string formatted for the trace log.
|
||||
*
|
||||
* @return a string formatted for the trace log.
|
||||
*/
|
||||
public String toString() {
|
||||
public String toTraceEvent() {
|
||||
String opcode = disassembleOp();
|
||||
StringBuilder sb = new StringBuilder(getInstructionByteStatus());
|
||||
sb.append(" ");
|
||||
sb.append(String.format("%-14s", opcode));
|
||||
sb.append("A:" + String.format("%02x", a) + " ");
|
||||
sb.append("X:" + String.format("%02x", x) + " ");
|
||||
sb.append("Y:" + String.format("%02x", y) + " ");
|
||||
sb.append("F:" + String.format("%02x", getStatusFlag()) + " ");
|
||||
sb.append("S:" + String.format("1%02x", sp) + " ");
|
||||
sb.append(getProcessorStatusString());
|
||||
sb.append("A:" + HexUtil.byteToHex(a) + " ");
|
||||
sb.append("X:" + HexUtil.byteToHex(x) + " ");
|
||||
sb.append("Y:" + HexUtil.byteToHex(y) + " ");
|
||||
sb.append("F:" + HexUtil.byteToHex(getStatusFlag()) + " ");
|
||||
sb.append("S:1" + HexUtil.byteToHex(sp) + " ");
|
||||
sb.append(getProcessorStatusString() + "\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@ -1395,11 +1400,17 @@ public class Cpu implements InstructionTable {
|
||||
switch (Cpu.instructionSizes[ir]) {
|
||||
case 0:
|
||||
case 1:
|
||||
return String.format("%04X %02X ", lastPc, ir);
|
||||
return HexUtil.wordToHex(lastPc) + " " +
|
||||
HexUtil.byteToHex(ir) + " ";
|
||||
case 2:
|
||||
return String.format("%04X %02X %02X ", lastPc, ir, args[0]);
|
||||
return HexUtil.wordToHex(lastPc) + " " +
|
||||
HexUtil.byteToHex(ir) + " " +
|
||||
HexUtil.byteToHex(args[0]) + " ";
|
||||
case 3:
|
||||
return String.format("%04X %02X %02X %02X", lastPc, ir, args[0], args[1]);
|
||||
return HexUtil.wordToHex(lastPc) + " " +
|
||||
HexUtil.byteToHex(ir) + " " +
|
||||
HexUtil.byteToHex(args[0]) + " " +
|
||||
HexUtil.byteToHex(args[1]);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -1422,35 +1433,35 @@ public class Cpu implements InstructionTable {
|
||||
|
||||
switch (instructionModes[ir]) {
|
||||
case ABS:
|
||||
sb.append(String.format(" $%04X", address(args[0], args[1])));
|
||||
sb.append(" $" + HexUtil.wordToHex(address(args[0], args[1])));
|
||||
break;
|
||||
case ABX:
|
||||
sb.append(String.format(" $%04X,X", address(args[0], args[1])));
|
||||
sb.append(" $" + HexUtil.wordToHex(address(args[0], args[1])) + ",X");
|
||||
break;
|
||||
case ABY:
|
||||
sb.append(String.format(" $%04X,Y", address(args[0], args[1])));
|
||||
sb.append(" $" + HexUtil.wordToHex(address(args[0], args[1])) + ",Y");
|
||||
break;
|
||||
case IMM:
|
||||
sb.append(String.format(" #$%02X", args[0]));
|
||||
sb.append(" #$" + HexUtil.byteToHex(args[0]));
|
||||
break;
|
||||
case IND:
|
||||
sb.append(String.format(" ($%04X)", address(args[0], args[1])));
|
||||
sb.append(" ($" + HexUtil.wordToHex(address(args[0], args[1])) + ")");
|
||||
break;
|
||||
case XIN:
|
||||
sb.append(String.format(" ($%02X,X)", args[0]));
|
||||
sb.append(" ($" + HexUtil.byteToHex(args[0]) + ",X)");
|
||||
break;
|
||||
case INY:
|
||||
sb.append(String.format(" ($%02X),Y", args[0]));
|
||||
sb.append(" ($" + HexUtil.byteToHex(args[0]) + "),Y");
|
||||
break;
|
||||
case REL:
|
||||
case ZPG:
|
||||
sb.append(String.format(" $%02X", args[0]));
|
||||
sb.append(" $" + HexUtil.byteToHex(args[0]));
|
||||
break;
|
||||
case ZPX:
|
||||
sb.append(String.format(" $%02X,X", a));
|
||||
sb.append(" $" + HexUtil.byteToHex(a) + ",X");
|
||||
break;
|
||||
case ZPY:
|
||||
sb.append(String.format(" $%02X,Y", a));
|
||||
sb.append(" $" + HexUtil.byteToHex(a) + ",Y");
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -247,6 +247,7 @@ public class Simulator implements Observer {
|
||||
// Spin up the new run loop
|
||||
runLoop = new RunLoop();
|
||||
runLoop.start();
|
||||
traceLog.simulatorDidStart();
|
||||
}
|
||||
|
||||
private void handleStop() {
|
||||
@ -268,6 +269,7 @@ public class Simulator implements Observer {
|
||||
statusPane.updateState(cpu);
|
||||
}
|
||||
});
|
||||
traceLog.simulatorDidStop();
|
||||
if (traceLog.isVisible()) {
|
||||
traceLog.refresh();
|
||||
}
|
||||
@ -291,6 +293,8 @@ public class Simulator implements Observer {
|
||||
ram.fill(0x00);
|
||||
// Clear the console.
|
||||
console.reset();
|
||||
// Reset the trace log.
|
||||
traceLog.reset();
|
||||
// Update status.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
@ -320,13 +324,9 @@ public class Simulator implements Observer {
|
||||
* Perform a single step of the simulated system.
|
||||
*/
|
||||
private void step() throws MemoryAccessException {
|
||||
|
||||
cpu.step();
|
||||
// TODO: We need to profile this for performance. Possibly allow
|
||||
// a flag to turn trace on/off
|
||||
synchronized(traceLog) {
|
||||
traceLog.append(cpu.getCpuState());
|
||||
}
|
||||
|
||||
traceLog.append(cpu.getCpuState());
|
||||
|
||||
// Read from the ACIA and immediately update the console if there's
|
||||
// output ready.
|
||||
|
@ -31,54 +31,99 @@ import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* This frame displays a trace of CPU execution. The most recent TRACE_LENGTH lines
|
||||
* This frame displays a trace of CPU execution. The most recent <code>TRACE_LENGTH</code> lines
|
||||
* are captured in a buffer and rendered to the JFrame's main text area upon request.
|
||||
*/
|
||||
public class TraceLog {
|
||||
|
||||
private FifoRingBuffer<Cpu.CpuState> traceLog;
|
||||
private JFrame traceLogWindow;
|
||||
private JTextArea logArea;
|
||||
private JFrame traceLogFrame;
|
||||
private JTextArea traceLogTextArea;
|
||||
|
||||
private static final Dimension SIZE = new Dimension(640, 480);
|
||||
private static final int MAX_LOG_LENGTH = 10000;
|
||||
private static final Dimension MIN_SIZE = new Dimension(320, 200);
|
||||
private static final Dimension PREFERRED_SIZE = new Dimension(640, 480);
|
||||
private static final int MAX_LOG_LENGTH = 50000;
|
||||
|
||||
public TraceLog() {
|
||||
traceLog = new FifoRingBuffer<Cpu.CpuState>(MAX_LOG_LENGTH);
|
||||
traceLogWindow = new JFrame();
|
||||
traceLogWindow.setPreferredSize(SIZE);
|
||||
traceLogWindow.setResizable(true);
|
||||
traceLogFrame = new JFrame();
|
||||
traceLogFrame.setMinimumSize(MIN_SIZE);
|
||||
traceLogFrame.setPreferredSize(PREFERRED_SIZE);
|
||||
traceLogFrame.setResizable(true);
|
||||
traceLogFrame.setTitle("Trace Log");
|
||||
|
||||
traceLogWindow.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||
traceLogFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
|
||||
|
||||
logArea = new JTextArea();
|
||||
logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||
traceLogTextArea = new JTextArea();
|
||||
traceLogTextArea.setFont(new Font(Font.MONOSPACED, Font.BOLD, 12));
|
||||
traceLogTextArea.setEditable(false);
|
||||
|
||||
JScrollPane scrollableView = new JScrollPane(logArea);
|
||||
JScrollPane scrollableView = new JScrollPane(traceLogTextArea);
|
||||
|
||||
traceLogWindow.getContentPane().add(scrollableView);
|
||||
traceLogWindow.pack();
|
||||
traceLogFrame.getContentPane().add(scrollableView);
|
||||
traceLogFrame.pack();
|
||||
// Don't show the frame. That action is controlled by the Simulator.
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraw the display with the most recent <code>MAX_LOG_LENGTH</code>
|
||||
* trace events. <strong>CAUTION</strong>: This can be a very expensive
|
||||
* call.
|
||||
*/
|
||||
public void refresh() {
|
||||
StringBuilder logString = new StringBuilder();
|
||||
for (Cpu.CpuState state : traceLog) {
|
||||
logString.append(state.toString());
|
||||
logString.append("\n");
|
||||
synchronized (this) {
|
||||
StringBuilder logString = new StringBuilder();
|
||||
for (Cpu.CpuState state : traceLog) {
|
||||
logString.append(state.toTraceEvent());
|
||||
}
|
||||
traceLogTextArea.setText(logString.toString());
|
||||
}
|
||||
logArea.setText(logString.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the log area.
|
||||
*/
|
||||
public void reset() {
|
||||
synchronized (this) {
|
||||
traceLog.reset();
|
||||
traceLogTextArea.setText("");
|
||||
traceLogTextArea.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a CPU State to the trace log.
|
||||
*
|
||||
* @param state The CPU State to append.
|
||||
*/
|
||||
public void append(Cpu.CpuState state) {
|
||||
traceLog.push(new Cpu.CpuState(state));
|
||||
synchronized(this) {
|
||||
traceLog.push(new Cpu.CpuState(state));
|
||||
}
|
||||
}
|
||||
|
||||
public void simulatorDidStart() {
|
||||
traceLogTextArea.setEnabled(false);
|
||||
}
|
||||
|
||||
public void simulatorDidStop() {
|
||||
traceLogTextArea.setEnabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Trace Log window is currently visible.
|
||||
* @return ture if the Trace Log window is currently visible.
|
||||
*/
|
||||
public boolean isVisible() {
|
||||
return traceLogWindow.isVisible();
|
||||
return traceLogFrame.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the Trace Log window.
|
||||
*
|
||||
* @param b True to make the Trace Log window visible, false to hide it.
|
||||
*/
|
||||
public void setVisible(boolean b) {
|
||||
traceLogWindow.setVisible(b);
|
||||
traceLogFrame.setVisible(b);
|
||||
}
|
||||
}
|
||||
|
77
src/main/java/com/loomcom/symon/util/HexUtil.java
Normal file
77
src/main/java/com/loomcom/symon/util/HexUtil.java
Normal file
@ -0,0 +1,77 @@
|
||||
package com.loomcom.symon.util;
|
||||
|
||||
/**
|
||||
* Hex String Utilities.
|
||||
*
|
||||
* <p/>
|
||||
*
|
||||
* But why? Java, after all, has a number of ways to convert an integer into a hex string,
|
||||
* so it may look absurd to go to the trouble of writing yet another conversion! The answer is
|
||||
* performance.
|
||||
*
|
||||
* <p/>
|
||||
*
|
||||
* The most convenient way to get a formatted hex value from an integer is with the <code>String.format</code>
|
||||
* method, but this turns out to be extremely inefficient. Formatting a million integers
|
||||
* with <code>String.format</code> takes something like 1600ms. Formatting the same number of integers
|
||||
* with <code>HexUtil</code> takes only 160ms. This is on part with <code>Integer.toHexString</code>,
|
||||
* but also allows the desired padding.
|
||||
*
|
||||
*/
|
||||
public class HexUtil {
|
||||
private static final String[] HEX_CONSTANTS = {"00", "01", "02", "03", "04", "05", "06", "07",
|
||||
"08", "09", "0A", "0B", "0C", "0D", "0E", "0F",
|
||||
"10", "11", "12", "13", "14", "15", "16", "17",
|
||||
"18", "19", "1A", "1B", "1C", "1D", "1E", "1F",
|
||||
"20", "21", "22", "23", "24", "25", "26", "27",
|
||||
"28", "29", "2A", "2B", "2C", "2D", "2E", "2F",
|
||||
"30", "31", "32", "33", "34", "35", "36", "37",
|
||||
"38", "39", "3A", "3B", "3C", "3D", "3E", "3F",
|
||||
"40", "41", "42", "43", "44", "45", "46", "47",
|
||||
"48", "49", "4A", "4B", "4C", "4D", "4E", "4F",
|
||||
"50", "51", "52", "53", "54", "55", "56", "57",
|
||||
"58", "59", "5A", "5B", "5C", "5D", "5E", "5F",
|
||||
"60", "61", "62", "63", "64", "65", "66", "67",
|
||||
"68", "69", "6A", "6B", "6C", "6D", "6E", "6F",
|
||||
"70", "71", "72", "73", "74", "75", "76", "77",
|
||||
"78", "79", "7A", "7B", "7C", "7D", "7E", "7F",
|
||||
"80", "81", "82", "83", "84", "85", "86", "87",
|
||||
"88", "89", "8A", "8B", "8C", "8D", "8E", "8F",
|
||||
"90", "91", "92", "93", "94", "95", "96", "97",
|
||||
"98", "99", "9A", "9B", "9C", "9D", "9E", "9F",
|
||||
"A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7",
|
||||
"A8", "A9", "AA", "AB", "AC", "AD", "AE", "AF",
|
||||
"B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7",
|
||||
"B8", "B9", "BA", "BB", "BC", "BD", "BE", "BF",
|
||||
"C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7",
|
||||
"C8", "C9", "CA", "CB", "CC", "CD", "CE", "CF",
|
||||
"D0", "D1", "D2", "D3", "D4", "D5", "D6", "D7",
|
||||
"D8", "D9", "DA", "DB", "DC", "DD", "DE", "DF",
|
||||
"E0", "E1", "E2", "E3", "E4", "E5", "E6", "E7",
|
||||
"E8", "E9", "EA", "EB", "EC", "ED", "EE", "EF",
|
||||
"F0", "F1", "F2", "F3", "F4", "F5", "F6", "F7",
|
||||
"F8", "F9", "FA", "FB", "FC", "FD", "FE", "FF"};
|
||||
|
||||
/**
|
||||
* Very fast 8-bit int to hex conversion, with zero-padded output.
|
||||
*
|
||||
* @param val The unsigned 8-bit value to convert to a zero padded hexadecimal string.
|
||||
* @return Two digit, zero padded hexadecimal string.
|
||||
*/
|
||||
public static String byteToHex(int val) {
|
||||
return HEX_CONSTANTS[val & 0xff];
|
||||
}
|
||||
|
||||
/**
|
||||
* Very fast 16-bit int to hex conversion, with zero-padded output.
|
||||
*
|
||||
* @param val The unsigned 16-bit value to convert to a zero padded hexadecimal string.
|
||||
* @return Four digit, zero padded hexadecimal string.
|
||||
*/
|
||||
public static String wordToHex(int val) {
|
||||
StringBuilder sb = new StringBuilder(4);
|
||||
sb.append(HEX_CONSTANTS[(val >> 8) & 0xff]);
|
||||
sb.append(HEX_CONSTANTS[val & 0xff]);
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
36
src/test/java/com/loomcom/symon/HexUtilTest.java
Normal file
36
src/test/java/com/loomcom/symon/HexUtilTest.java
Normal file
@ -0,0 +1,36 @@
|
||||
package com.loomcom.symon;
|
||||
|
||||
import com.loomcom.symon.util.HexUtil;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class HexUtilTest extends TestCase {
|
||||
public void testByteToHex() {
|
||||
assertEquals("FE", HexUtil.byteToHex(0xfe));
|
||||
assertEquals("00", HexUtil.byteToHex(0));
|
||||
assertEquals("0A", HexUtil.byteToHex(10));
|
||||
}
|
||||
|
||||
public void testByteToHexIgnoresSign() {
|
||||
assertEquals("FF", HexUtil.byteToHex(-1));
|
||||
}
|
||||
|
||||
public void testByteToHexMasksLowByte() {
|
||||
assertEquals("FE", HexUtil.byteToHex(0xfffe));
|
||||
assertEquals("00", HexUtil.byteToHex(0xff00));
|
||||
}
|
||||
|
||||
public void testWordToHex() {
|
||||
assertEquals("0000", HexUtil.wordToHex(0));
|
||||
assertEquals("FFFF", HexUtil.wordToHex(65535));
|
||||
assertEquals("FFFE", HexUtil.wordToHex(65534));
|
||||
}
|
||||
|
||||
public void testWordToHexIgnoresSign() {
|
||||
assertEquals("FFFF", HexUtil.wordToHex(-1));
|
||||
}
|
||||
|
||||
public void testWordToHexMasksTwoLowBytes() {
|
||||
assertEquals("FFFE", HexUtil.wordToHex(0xfffffe));
|
||||
assertEquals("FF00", HexUtil.wordToHex(0xffff00));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user