From 541f825b9d3d013c8cb668d5bf10709ab45dbde5 Mon Sep 17 00:00:00 2001 From: cl507523 Date: Wed, 25 Feb 2026 07:10:40 +0000 Subject: [PATCH] fix(evm): cache fallback decisions and tune JIT suitability thresholds Two fixes in the ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK mechanism: 1. Per-call analyzer overhead: EVMAnalyzer was constructed on every execute() call, rebuilding a std::map over the full bytecode each time. For benchmarks running thousands of iterations this added ~245us per call even when no fallback was needed. Fix: cache the ShouldFallback boolean per module key (CRC32 + revision) in a new FallbackCache map on the DTVM VM struct. 2. Tuned fallback thresholds to eliminate false positives while covering all pathological cases: - Removed MirEstimate condition (caused false positives on PUSH22-32 and other harmless synth benchmarks). - Raised BytecodeSize limit from 0x6000 (24 576) to 0x10000 (64 KB). The old limit overlapped the EIP-170 deployed-code ceiling and caused false positives on real contracts (snailtracer, MUL/b1, etc). The new limit is safely above both EIP-170 (24 576) and the EIP-3860 initcode limit (49 152), while still catching extreme synthetic test bytecode (402 KB, 6 MB) that would stall LLVM. - Pattern-based thresholds (MaxConsecutiveExpensive > 128, MaxBlockExpensiveCount > 256, DupFeedbackPatternCount > 64) remain and precisely target the 5 pathological benchmark cases. Verified: all 223 evmonetestsuite multipass tests pass; large synthetic bytecode cases (evmone_block_gas_cost_overflow_*) correctly fall back to interpreter; 207/216 common benchmark cases remain within 20% of the no-fallback JIT reference. Co-authored-by: Cursor --- src/compiler/evm_frontend/evm_analyzer.h | 15 ++++++++---- src/vm/dt_evmc_vm.cpp | 30 ++++++++++++++---------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/compiler/evm_frontend/evm_analyzer.h b/src/compiler/evm_frontend/evm_analyzer.h index c5af915c..85e3d11d 100644 --- a/src/compiler/evm_frontend/evm_analyzer.h +++ b/src/compiler/evm_frontend/evm_analyzer.h @@ -114,7 +114,10 @@ struct JITSuitabilityResult { /// Thresholds for JIT suitability fallback. Normal contracts have <20 /// RA-expensive ops per block; these values are conservatively high. -static constexpr size_t MAX_JIT_BYTECODE_SIZE = 0x6000; +/// Bytecode size limit: 64 KB is well above the EIP-170 deployed-code limit +/// (24 576) and the EIP-3860 initcode limit (49 152), so real-world contracts +/// are never affected, while pathological synthetic tests (400 KB+) are caught. +static constexpr size_t MAX_JIT_BYTECODE_SIZE = 0x10000; static constexpr size_t MAX_JIT_MIR_ESTIMATE = 50000; static constexpr size_t MAX_CONSECUTIVE_RA_EXPENSIVE = 128; static constexpr size_t MAX_BLOCK_RA_EXPENSIVE = 256; @@ -306,10 +309,14 @@ class EVMAnalyzer { BlockInfos.emplace(CurInfo.EntryPC, CurInfo); } - // Compute final fallback verdict + // Compute final fallback verdict. + // Pattern-based thresholds target pathological LLVM register-allocator + // cases (dense RA-expensive opcodes, DUP feedback loops). + // The bytecode size guard (64 KB) catches extreme synthetic tests whose + // sheer IR volume stalls LLVM, without affecting real contracts (bounded + // by EIP-170 / EIP-3860). JITResult.ShouldFallback = - BytecodeSize > MAX_JIT_BYTECODE_SIZE || - JITResult.MirEstimate > MAX_JIT_MIR_ESTIMATE || + JITResult.BytecodeSize > MAX_JIT_BYTECODE_SIZE || JITResult.MaxConsecutiveExpensive > MAX_CONSECUTIVE_RA_EXPENSIVE || JITResult.MaxBlockExpensiveCount > MAX_BLOCK_RA_EXPENSIVE || JITResult.DupFeedbackPatternCount > MAX_DUP_FEEDBACK_PATTERN; diff --git a/src/vm/dt_evmc_vm.cpp b/src/vm/dt_evmc_vm.cpp index 2ee646ba..abeae304 100644 --- a/src/vm/dt_evmc_vm.cpp +++ b/src/vm/dt_evmc_vm.cpp @@ -79,6 +79,9 @@ struct DTVM : evmc_vm { std::unique_ptr RT; std::unique_ptr ExecHost; std::unordered_map LoadedMods; +#ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK + std::unordered_map FallbackCache; +#endif Isolation *Iso = nullptr; }; @@ -148,28 +151,29 @@ evmc_result execute(evmc_vm *EVMInstance, const evmc_host_interface *Host, return evmc_make_result(EVMC_FAILURE, 0, 0, nullptr, 0); } } + uint32_t CheckSum = crc32(Code, CodeSize); + uint64_t ModKey = (static_cast(Rev) << 32) | CheckSum; + #ifdef ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK - // Use interpreter mode for bytecode that would be too expensive to JIT. - // The EVMAnalyzer performs a pattern-aware O(n) scan that detects: - // - raw bytecode size / estimated MIR instruction count too large - // - high density of RA-expensive opcodes (SHL/SHR/SAR/MUL/SIGNEXTEND) - // - long consecutive runs of RA-expensive ops - // - DUP-induced feedback loops (b0 pattern) std::unique_ptr TempConfig; if (VM->Config.Mode == RunMode::MultipassMode) { - COMPILER::EVMAnalyzer Analyzer(Rev); - Analyzer.analyze(Code, CodeSize); - const auto &JITResult = Analyzer.getJITSuitability(); - if (JITResult.ShouldFallback) { + auto CacheIt = VM->FallbackCache.find(ModKey); + bool NeedFallback; + if (CacheIt != VM->FallbackCache.end()) { + NeedFallback = CacheIt->second; + } else { + COMPILER::EVMAnalyzer Analyzer(Rev); + Analyzer.analyze(Code, CodeSize); + NeedFallback = Analyzer.getJITSuitability().ShouldFallback; + VM->FallbackCache[ModKey] = NeedFallback; + } + if (NeedFallback) { RuntimeConfig NewConfig = VM->Config; NewConfig.Mode = RunMode::InterpMode; TempConfig = std::make_unique(VM->RT.get(), NewConfig); } } #endif // ZEN_ENABLE_JIT_PRECOMPILE_FALLBACK - - uint32_t CheckSum = crc32(Code, CodeSize); - uint64_t ModKey = (static_cast(Rev) << 32) | CheckSum; std::string ModName = std::to_string(CheckSum) + "_" + std::to_string(static_cast(Rev)); auto ModRet = VM->RT->loadEVMModule(ModName, Code, CodeSize, Rev);