The Playdate handheld console's CPU operates very fast but accessing main memory, especially when data is not already in the cache, is comparatively slow. This asymmetry means that programs that keep their working data in registers and keep their most-frequently-executed code inside the limited instruction cache can run at near-peak speed, even if they contain many loops or suffer branch mispredictions. Consequently, developers aiming for high-performance workloads such as emulators or simulators must prioritize minimizing traffic to slow main memory by maximizing register usage and making the core loop fit tightly within the instruction cache.
To shrink the instruction-cache footprint, the author recommends compiling with -Os instead of -O3, trimming large switch tables into fewer branches, and isolating rarely-used opcodes or edge cases outside the hot core. By marking functions with __attribute__((section (``.text.<name>'')) ) and using a custom linker script copied from the SDK's C_API/buildsupport folder, the linker can place all core functions in a single contiguous block, whose size can be inspected with nm Source/pdex.elf | sort > syms.txt. For data, the tightly-coupled memory (TCM) region—where the hardware stack resides—offers faster access than ordinary RAM; stack-allocated objects therefore run quicker, and developers can memcpy heap or static structures into a reserved low-stack area (the dtcm_mempool) for the duration of a computation, copying the result back afterward. Similarly, functions can be marked for the instruction TCM (ITCM) region via a macro that adds __attribute__((section (``.itcm. '')) ) __attribute__((short_call)), copied from .text to TCM at runtime with careful attention to address congruence modulo 2, short_call/long_call attributes, and flushing the instruction cache after the copy.
Even after applying the above tricks, builds can exhibit a ``performance lottery'' where seemingly unrelated changes cause large, unpredictable swings in speed. This stems from two hardware effects: cache-line misalignment (the 32-byte line size means that a function whose start address is not a multiple of 32 may straddle two lines, pulling in unwanted neighbours or leaving useful code uncached) and the branch-prediction table, where instruction addresses that differ by multiples of 1024 may share the same predictor entries, leading to mispredictions. Mitigation involves deliberately aligning functions and loops to 32-byte boundaries in the linker map (. = ALIGN(32); . += offset) or with compiler flags like -falign-loops=32, and sprinkling . = ALIGN(1024); . += n choices based on a known-good build to decorrelate predictor indices. While prefetching has shown limited benefit on the Playdate, these alignment techniques have largely eliminated the performance-lottery effect in practice.