diff --git a/ir/opt/ssa_opt.c b/ir/opt/ssa_opt.c index bc07f232..2e8131ac 100644 --- a/ir/opt/ssa_opt.c +++ b/ir/opt/ssa_opt.c @@ -750,7 +750,13 @@ int tcc_ir_ssa_opt_run(IRSSAOptCtx *ctx) SSA_RUN("ssa:narrow", ssa_opt_narrow(ctx)); SSA_RUN("ssa:gvn", ssa_opt_gvn(ctx)); SSA_RUN("ssa:phi_simplify", ssa_opt_phi_simplify(ctx)); - SSA_RUN("ssa:dead_loop", ssa_opt_dead_loop(ctx)); + /* Dead-loop post-phi rewrite: collapse a side-effect-free counting loop + * whose result is a loop-invariant constant into a guarded constant + * (`(trip>0) ? body_const : init`). This is an -O2 optimization — GCC + * likewise keeps the empty loop at -O1 and only elides it at -O2 — so + * gate it to keep -O1 a lighter tier. */ + SSA_RUN("ssa:dead_loop", + (tcc_state && tcc_state->optimize >= 2) ? ssa_opt_dead_loop(ctx) : 0); SSA_RUN("ssa:dce", ssa_opt_dce(ctx)); /* target-specific generators (registered by backend) */ diff --git a/libtcc.c b/libtcc.c index 49664f39..e4ecb340 100644 --- a/libtcc.c +++ b/libtcc.c @@ -2271,7 +2271,12 @@ PUB_FUNC int tcc_parse_args(TCCState *s, int *pargc, char ***pargv, int optind) break; case TCC_OPTION_O: s->optimize = atoi(optarg); - /* Enable all IR optimizations when -O1 or higher */ + /* -O1: scalar cleanup, cheap ARM addressing-mode fusion, constant/range + * folding, loop rotation, and light inlining of explicitly-inline / tiny + * functions — the same class of work GCC does at -O1. The heavy tier + * (loop unrolling / IV strength reduction / LICM, MUL+ADD→MLA fusion, + * interprocedural const-prop, re-rolling, and full small-function + * inlining) is gated to -O2. */ if (s->optimize >= 1) { s->opt_dce = 1; @@ -2295,28 +2300,32 @@ PUB_FUNC int tcc_parse_args(TCCState *s, int *pargc, char ***pargv, int optind) * (tcc froze in parse_number on every integer literal). * Without the fusion `*p++` lowers to an explicit * LOAD + ADD whose result is written back correctly. */ - s->opt_mla_fusion = 1; /* Fuse MUL+ADD into MLA */ - /* fp-offset-cache disabled: miscompiles loops when combined with - iv-strength-red (e.g. SHA-1 sha_transform). Can still be - enabled manually with -ffp-offset-cache for debugging. */ s->opt_stack_addr_cse = 1; /* Hoist repeated stack address computations */ - s->opt_licm = 1; /* Loop-invariant code motion */ - s->opt_ipc = 1; /* Interprocedural constant propagation */ - s->opt_strength_red = 1; /* Strength reduction for multiply */ - s->opt_iv_strength_red = 1; /* IV strength reduction for array loops */ - s->opt_loop_unroll = 1; /* Full-unroll small constant-trip-count loops */ - s->opt_loop_rotation = 1; /* Rotate top-tested loops to bottom-tested */ - s->opt_reroll = 1; /* Re-roll runs of identical macro-unrolled blocks */ + s->opt_strength_red = 1; /* Strength reduction for multiply (peephole) */ s->opt_nonneg_fold = 1; /* Non-negative value branch folding */ s->opt_vrp = 1; /* Value range propagation branch folding */ s->opt_float_narrow = 1; /* Narrow double math to float when safe */ s->opt_jump_threading = 1; /* Jump threading optimization */ + s->opt_loop_rotation = 1; /* Rotate top-tested loops to bottom-tested */ s->opt_inline_small = 1; /* Inline tiny static/inline functions (≤30 words) */ if (!s->opt_inline_limit) s->opt_inline_limit = 30; } + /* -O2: everything in -O1 plus the heavy tier — loop unrolling / IV + * strength reduction / LICM / re-rolling, MUL+ADD→MLA fusion, + * interprocedural constant propagation, and full small-function + * inlining. */ if (s->optimize >= 2) { + s->opt_mla_fusion = 1; /* Fuse MUL+ADD into MLA */ + /* fp-offset-cache disabled: miscompiles loops when combined with + iv-strength-red (e.g. SHA-1 sha_transform). Can still be + enabled manually with -ffp-offset-cache for debugging. */ + s->opt_licm = 1; /* Loop-invariant code motion */ + s->opt_ipc = 1; /* Interprocedural constant propagation */ + s->opt_iv_strength_red = 1; /* IV strength reduction for array loops */ + s->opt_loop_unroll = 1; /* Full-unroll small constant-trip-count loops */ + s->opt_reroll = 1; /* Re-roll runs of identical macro-unrolled blocks */ s->opt_inline_functions = 1; /* Inline small static/inline functions (≤100 words) */ if (s->opt_inline_limit < 100) s->opt_inline_limit = 100; diff --git a/metrics/schema.sql b/metrics/schema.sql index 954ef0bd..7c682d55 100644 --- a/metrics/schema.sql +++ b/metrics/schema.sql @@ -11,7 +11,12 @@ -- -- Apply with: sqlite3 metrics.db < metrics/schema.sql (safe to re-run). -PRAGMA journal_mode = WAL; -- Grafana reads never block the recorder's writes +-- NOT WAL: Grafana bind-mounts this file read-only (docker-compose.yml `:ro`), +-- and SQLite cannot open a WAL database read-only -- even a SELECT must write the +-- -wal/-shm sidecars, which fails with "attempt to write a readonly database". +-- Rollback-journal mode reads fine from read-only media; at a once-per-commit +-- write cadence the recorder (busy timeout 60s) and the dashboard never contend. +PRAGMA journal_mode = DELETE; PRAGMA foreign_keys = ON; -- One row per (commit, host). parent_sha = first parent, used by the diff --git a/tccgen.c b/tccgen.c index 3cfc17f3..42964d63 100644 --- a/tccgen.c +++ b/tccgen.c @@ -30526,7 +30526,10 @@ static void gen_function(Sym *sym) * transformations and constant propagation have simplified loop bodies. */ if (tcc_state->opt_dce) { - int dle_changes = tcc_ir_opt_dead_loop_elim(ir); + /* Dead-loop elimination collapses a side-effect-free loop into its final + * (constant) result. Gate to -O2 so -O1 keeps the loop, matching GCC's + * -O1 (which also only elides such loops at -O2). */ + int dle_changes = (tcc_state->optimize >= 2) ? tcc_ir_opt_dead_loop_elim(ir) : 0; if (dle_changes > 0) { tcc_ir_opt_value_tracking(ir); @@ -30555,7 +30558,10 @@ static void gen_function(Sym *sym) tcc_ir_opt_dead_var_store_elim(ir); tcc_ir_opt_dse(ir); } - int dle_changes = tcc_ir_opt_dead_loop_elim(ir); + /* Dead-loop elimination collapses a side-effect-free loop into its final + * (constant) result. Gate to -O2 so -O1 keeps the loop, matching GCC's + * -O1 (which also only elides such loops at -O2). */ + int dle_changes = (tcc_state->optimize >= 2) ? tcc_ir_opt_dead_loop_elim(ir) : 0; if (dle_changes > 0) { tcc_ir_opt_branch_folding(ir); diff --git a/tests/benchmarks/run_benchmark.py b/tests/benchmarks/run_benchmark.py index a8292e82..254dcdf4 100755 --- a/tests/benchmarks/run_benchmark.py +++ b/tests/benchmarks/run_benchmark.py @@ -10,6 +10,7 @@ import argparse import os +import shutil import socket import subprocess import sys @@ -219,12 +220,15 @@ def build_compiler(compiler: str, ssh_host: str, opt_level: str = "1") -> Tuple[ if tcc_path: print(f"Using TCC: {tcc_path}") if check_compiler_changed(build_dir, compiler): - print(f"TCC compiler has been updated, forcing reconfiguration...") + print(f"TCC compiler has been updated, forcing a clean rebuild...") force_reconfigure = True - # Remove CMake cache to force reconfiguration - cmake_cache = build_dir / "CMakeCache.txt" - if cmake_cache.exists(): - cmake_cache.unlink() + # The compiler binary changed but the .c sources did not, so CMake's + # incremental build would happily reuse objects compiled by the OLD + # compiler (make only tracks source mtimes, not the compiler's). That + # silently benchmarks stale code. Wipe the whole build tree so every + # object — benchmark and SDK — is recompiled with the new compiler. + shutil.rmtree(build_dir, ignore_errors=True) + build_dir.mkdir(parents=True, exist_ok=True) # Set environment with PICO_SDK_PATH env = os.environ.copy()