From 807a43ce6fd1bb57a2e3598ae9276209299928ad Mon Sep 17 00:00:00 2001 From: Seth Morabito Date: Sun, 9 Dec 2012 21:02:11 -0800 Subject: [PATCH] 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. --- src/main/java/com/loomcom/symon/Cpu.java | 67 ++++++++------ .../java/com/loomcom/symon/Simulator.java | 12 +-- .../java/com/loomcom/symon/ui/TraceLog.java | 89 ++++++++++++++----- .../java/com/loomcom/symon/util/HexUtil.java | 77 ++++++++++++++++ .../java/com/loomcom/symon/HexUtilTest.java | 36 ++++++++ 5 files changed, 225 insertions(+), 56 deletions(-) create mode 100644 src/main/java/com/loomcom/symon/util/HexUtil.java create mode 100644 src/test/java/com/loomcom/symon/HexUtilTest.java diff --git a/src/main/java/com/loomcom/symon/Cpu.java b/src/main/java/com/loomcom/symon/Cpu.java index 03d7576..e4eb9dc 100644 --- a/src/main/java/com/loomcom/symon/Cpu.java +++ b/src/main/java/com/loomcom/symon/Cpu.java @@ -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 Clonable + * 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; } diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java index df8d16f..cf58a1f 100644 --- a/src/main/java/com/loomcom/symon/Simulator.java +++ b/src/main/java/com/loomcom/symon/Simulator.java @@ -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. diff --git a/src/main/java/com/loomcom/symon/ui/TraceLog.java b/src/main/java/com/loomcom/symon/ui/TraceLog.java index 85c05b1..9641fa9 100644 --- a/src/main/java/com/loomcom/symon/ui/TraceLog.java +++ b/src/main/java/com/loomcom/symon/ui/TraceLog.java @@ -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 TRACE_LENGTH lines * are captured in a buffer and rendered to the JFrame's main text area upon request. */ public class TraceLog { private FifoRingBuffer 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(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 MAX_LOG_LENGTH + * trace events. CAUTION: 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); } } diff --git a/src/main/java/com/loomcom/symon/util/HexUtil.java b/src/main/java/com/loomcom/symon/util/HexUtil.java new file mode 100644 index 0000000..b03c92b --- /dev/null +++ b/src/main/java/com/loomcom/symon/util/HexUtil.java @@ -0,0 +1,77 @@ +package com.loomcom.symon.util; + +/** + * Hex String Utilities. + * + *

+ * + * 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. + * + *

+ * + * The most convenient way to get a formatted hex value from an integer is with the String.format + * method, but this turns out to be extremely inefficient. Formatting a million integers + * with String.format takes something like 1600ms. Formatting the same number of integers + * with HexUtil takes only 160ms. This is on part with Integer.toHexString, + * 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(); + } +} \ No newline at end of file diff --git a/src/test/java/com/loomcom/symon/HexUtilTest.java b/src/test/java/com/loomcom/symon/HexUtilTest.java new file mode 100644 index 0000000..7dfa55a --- /dev/null +++ b/src/test/java/com/loomcom/symon/HexUtilTest.java @@ -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)); + } +}