The decoder tables are built from one parse of each instruction pattern

This commit is contained in:
quatrixone authored and BlackBearReloaded committed 2026-10-02 15:58:44 -04:00
1 parent 66fe402d5b
commit 2baaf548ae
3 files changed
+66

No files matched your search

+28
View File
@@ -291,6 +291,34 @@ list(FILTER jit_sources EXCLUDE REGEX "(^|/)a64_translate\\.cpp$")
set_property(TARGET dynarmic PROPERTY SOURCES "${jit_sources}")
target_sources(dynarmic PRIVATE "${PORT_BUILD_DIR}/a64_translate.cpp")
# Decoder tables: upstream fills 4096 lists and, for each list, parses the bit string of every
# instruction pattern again (~640 for A64, ~260 for A32): about half a second in the first block
# a game compiles, on the guest core that asked for it. Parse every pattern once, then fill the
# lists from that, in the same order (tools/check-decoder-startup.py).
foreach(decoder IN ITEMS "A64/decoder/a64" "A32/decoder/arm")
set(decoder_source "${PROJECT_SOURCE_DIR}/src/dynarmic/src/dynarmic/frontend/${decoder}")
file(READ "${decoder_source}.h" decoder_header)
get_filename_component(decoder_name "${decoder}" NAME)
set(decoder_loop " for (size_t i = 0; i < t.size(); ++i) {\n#define INST")
set(decoder_match " if ((i & make_fast_index(mask)) == make_fast_index(expect)) { \\\n t[i].emplace_back(")
set(decoder_end "#include \"./${decoder_name}.inc\"\n#undef INST\n }\n return t;")
foreach(anchor IN ITEMS decoder_loop decoder_match decoder_end)
string(FIND "${decoder_header}" "${${anchor}}" offset)
if(offset LESS 0)
message(FATAL_ERROR "Pinned ${decoder_name} decoder table changed (${anchor})")
endif()
endforeach()
string(REPLACE "${decoder_loop}" " std::vector<Handler> all;\n {\n#define INST" decoder_header "${decoder_header}")
string(REPLACE "${decoder_match}" " { \\\n all.emplace_back(" decoder_header "${decoder_header}")
string(REPLACE "${decoder_end}" "#include \"${decoder_source}.inc\"\n#undef INST\n }\n for (size_t i = 0; i < t.size(); ++i)\n for (auto const& e : all)\n if ((i & make_fast_index(e.mask)) == make_fast_index(e.expect))\n t[i].push_back(e);\n return t;" decoder_header "${decoder_header}")
# The other use of the pattern list in the header (the instruction's name).
string(REPLACE "#include \"./${decoder_name}.inc\"" "#include \"${decoder_source}.inc\"" decoder_header "${decoder_header}")
write_derived("${PORT_BUILD_DIR}/include/dynarmic/frontend/${decoder}.h" "${decoder_header}")
endforeach()
# The override headers shadow upstream's only by include order: a definition on the target
# recompiles every dynarmic object that saw the upstream ones.
target_compile_definitions(dynarmic PRIVATE EDEN_DECODER_ONE_PARSE=1)
# Cancel low-lane extraction of a scalar that was just zero-extended to Q.
# Preserve other lanes, arbitrary vectors and all floating-point operations.
file(READ "${PROJECT_SOURCE_DIR}/src/dynarmic/src/dynarmic/ir/opt_passes.cpp" optimizer)
+1
View File
@@ -66,6 +66,7 @@ python3 -B "$root/tools/check-heap-growth.py"
python3 -B "$root/tools/check-crash-report.py"
"$scratch/build/bin/eden-scalar-check"
"$scratch/build/bin/eden-memory-check"
python3 -B "$root/tools/check-decoder-startup.py"
python3 -B "$root/headless/check_audio_shutdown.py" "$scratch/build/headless/core.cpp" "$scratch/source/src/core/core.cpp"
"$scratch/build/bin/eden-romfs-check"
"$scratch/build/bin/eden-ryujinx-check"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""The first block a game compiles must not pay for the instruction decoder's table.
Dynarmic builds a table of 4096 lists from its ~640 A64 (and ~260 A32) instruction patterns the
first time a block is translated. Upstream parses every pattern's bit string again for each of
the 4096 lists: about half a second on the first compilation, on the guest core that asked for
it. The port parses each pattern once (headless/CMakeLists.txt, decoder tables).
This runs the host build's cold-compilation benchmark (eden-memory-check --cold-compile): three
times the same 8192 blocks, each in a new JIT. Only the first builds the table, so it may not
take much longer than the others. It also checks that both derived decoders have the one-parse
form and list the handlers in upstream's order.
"""
from pathlib import Path
import re
import subprocess
root = Path(__file__).resolve().parents[1]
cache = Path((root / '.local/headless-cache').read_text().strip())
for name in ('A64/decoder/a64.h', 'A32/decoder/arm.h'):
derived = (cache / 'build/headless/include/dynarmic/frontend' / name).read_text()
assert 'for (size_t i = 0; i < t.size(); ++i) {\n#define INST' not in derived, name
assert 'all.emplace_back(' in derived and 't[i].push_back(e);' in derived, name
def trials():
output = subprocess.run([str(cache / 'build/bin/eden-memory-check'), '--cold-compile'],
capture_output=True, text=True, check=True, timeout=120).stdout
seconds = [float(s) for s in re.findall(r'COLD_COMPILE trial=\d blocks=8192 chain=1 seconds=([0-9.]+)', output)]
assert len(seconds) == 3, output
return seconds
# The best of three runs: another program's burst must not fail the build.
ratio, first, later = min((s[0] / min(s[1:]), s[0], min(s[1:])) for s in (trials() for _ in range(3)))
print(f'Decoder table: first compilation {first * 1000:.0f} ms, later {later * 1000:.0f} ms, ratio {ratio:.1f}')
assert ratio < 3.0, 'the first compilation still pays for the decoder table'
print('Decoder startup: the table is built from one parse of each pattern PASS')