Menu

#166 2K page size is inefficient

future
open
nobody
5
2026-08-24
2015-05-04
No

I'm raising this as a bug even though it might be argued that it should be a feature request, simply as it's a performance issue.

With the switch to 2KB pages (arguably needed for Opus, but also needed for the Mirage Microdriver, Currah µSpeech, Didaktik 80 (and 40), we incur a greater performance cost to page swapping.

The ZX Spectrum 48K, 128K and +2/+3 are the main concern for those running on mobile devices, where this overhead matters most. I therefore think we should revert to a 16 KB page size, but add a mechanism for handling subpages.

I could be talked into going with an 8 KB page size for the Timex machines but feel this would probably not be worth the slowdown to 48K/128K emulation.

I'm not even sure whether there are any popular pathological cases of page swapping for the 128K/+2/+3 to test against, so I am not sure of the severity of this. Even with 2K pages Fuse still only uses 2.5% CPU when at the 128K menu on my own machine so I'm not sure how bad the change to 2K pages will have been... so whilst I don't currently see this as a huge issue, I'd like to know if I'm mistaken in that view.

Once we have a subpage mechanism, we should be able to freely switch back to 4K, 8K, 16K or even 32K pages without breaking emulation.

I imagine we would have recursive divisions into groups of four or so subpages, so for example:

0x0000-0x0000 [0]: Opus ROMCS
    0x0000-0x0fff [0,0]: Lower Opus ROM
    0x1000-0x1fff [0,1]: Upper Opus ROM
    0x2000-0x2fff [0,2]: RAM/MMIO
        0x2000-0x23ff [0,2,0]: Lower Opus RAM
        0x2400-0x27ff [0,2,1]: Upper Opus RAM
        0x2800-0x2bff [0,2,2]: WD FDC MMIO
        0x2c00-0x2fff [0,2,3]: WD FDC MMIO
    0x3000-0x3fff [0,3]: MMIO/floating bus
        0x3000-0x33ff [0,3,0]: 6821 MMIO
        0x3400-0x37ff [0,3,1]: 6821 MMIO
        0x3800-0x3bff [0,3,2]: Floating bus
        0x3c00-0x3fff [0,3,3]: Floating bus
0x4000-0x7fff [1]: RAM page 5
0x8000-0xbfff [2]: RAM page 2
0xc000-0xffff [3]: RAM page 0

With such a scheme in place we would no longer need peripheral-specific code in writebyte_internal() or in readbyte().

The only problem here is I'm sketchy on the details of how to handle this efficiently. I imagine we would throw away our old page map entirely when updating our page tables, and not bother coalescing subpages if a fine grained mapping is overridden by a more coarse mapping.

For ROMs, we might want to consider achieving mirroring by duplicating the ROM image itself, to avoid using subpages.

Discussion

  • Stuart Brady

    Stuart Brady - 2015-05-04
    • Description has changed:

    Diff:

    --- old
    +++ new
    @@ -19,7 +19,7 @@
                 0x2000-0x23ff [0,2,0]: Lower Opus RAM
                 0x2400-0x27ff [0,2,1]: Upper Opus RAM
                 0x2800-0x2bff [0,2,2]: WD FDC MMIO
    
    -            0x2c00-0x2fff [0,2,3]: FDC MMIO
    +            0x2c00-0x2fff [0,2,3]: WD FDC MMIO
             0x3000-0x3fff [0,3]: MMIO/floating bus
                 0x3000-0x33ff [0,3,0]: 6821 MMIO
                 0x3400-0x37ff [0,3,1]: 6821 MMIO
    
     
  • Stuart Brady

    Stuart Brady - 2015-05-04
    • summary: Hierarchical page handling --> 2K page size is inefficient
     
  • Fredrick Meunier

    🤖 Repo Assist triage note for this long-open (2015) performance/design ticket.

    Current state in the repo: The 2 KB page size is still in force. memory_pages.h sets MEMORY_PAGE_SIZE_LOGARITHM 11 and carries the note: "we now rely on 2KB page size so this should no longer be changed without a full review of all relevant code and changes will be required to match". ChangeLog records the switch ("Switch to 2KB page size (Stuart Brady)"). So the requested revert to 16 KB pages is not currently on the table without a full architecture review.

    Your premise checks out against the code: a full 16K remap funnels through memory_map_16k_read_write -> memory_map_8k_read_write -> memory_map_4k_read_write -> memory_map_2k_read_write, which copies one memory_page struct (~28 bytes) per 2K slot. A 16K remap therefore copies 8 read + 8 write entries (16 struct copies) instead of 1 read + 1 write for 16 KB pages — 8× the remap work. The map arrays also carry 32 entries (~896 bytes) vs ~112 bytes for 4×16 KB entries, so a larger per-frame cache footprint too.

    But the practical impact looks bounded, consistent with your own 2.5% CPU measurement:

    • The per-access hot path cost is identical regardless of page size: readbyte_internal/writebyte_internal just do memory_map_read[address >> 11].page[address & mask]. Page size only affects remap cost and map-array footprint.
    • Page swaps occur only on OUT to 0x7ffd/0x1ffd (and ROMCS changes), which is comparatively rare — typically a handful per frame, not per instruction. Each 16K remap is a few struct copies, sub-microsecond.

    Suggested next steps if you want to quantify before a big refactor:

    1. Add a temporary counter of memory_map_*_read_write calls and total struct copies per second under a page-swap-heavy workload (128K/+2/+3 games that bank-switch frequently, or RZX playback) and compare the current 2K build against an experimental 16K build. That directly answers "how bad the 2K change is".
    2. If the numbers justify it, the recursive subpage scheme you outlined is the right shape. Note the current flat map is relied on by many peripherals (Opus, uSpeech, Spectranet, ttx2000s, ZXATASP/ZXCF, DIVXXX ROMCS) and by snapshot save/restore, so a recursive/subpage map is a correctness-sensitive change needing exactly the full review the header already warns about.

    Recommend keeping this open under needs triage/enhancement pending the benchmark; it is a performance design question rather than a regression.

     
  • Fredrick Meunier

    Ticket moved from /p/fuse-emulator/bugs/324/

     

Log in to post a comment.