1
0
mirror of https://github.com/sethm/symon.git synced 2024-06-01 08:41:32 +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:
Seth Morabito 2012-12-09 21:02:11 -08:00
parent bc3de80892
commit 807a43ce6f
5 changed files with 225 additions and 56 deletions

View File

@ -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;
}

View File

@ -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.

View File

@ -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);
}
}

View 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();
}
}

View 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));
}
}