diff --git a/changelog.d/8439-size-opt-default-on.md b/changelog.d/8439-size-opt-default-on.md new file mode 100644 index 0000000000..5a9b73180c --- /dev/null +++ b/changelog.d/8439-size-opt-default-on.md @@ -0,0 +1 @@ +Make `-Os` the default native optimization level. Measured on the quiet bench mini under the sweep's bench lock, `-Os` costs no runtime speed on the benchmark corpus — every delta across `churn`, `cycles`, `fib40`, `interp`, `iso_miss` and `tree_wide` was within 0.8% and none disjoint from the `-O3` arm — while #8418 measured a further 346.7 MiB off a 4,743-module generated bundle. `PERRY_LL_SIZE_OPT=0` (or `off`/`false`/`no`) restores `-O3`. diff --git a/crates/perry-codegen/src/linker.rs b/crates/perry-codegen/src/linker.rs index ab5f1089a8..ef0a2bb5a4 100644 --- a/crates/perry-codegen/src/linker.rs +++ b/crates/perry-codegen/src/linker.rs @@ -307,11 +307,24 @@ fn cpu_tuning_arg_for( } } +/// Size optimization is **on by default**. Measured on the quiet bench mini +/// under the sweep's bench lock, `-Os` costs no measurable runtime speed: across +/// `churn`, `cycles`, `fib40`, `interp`, `iso_miss` and `tree_wide` every delta +/// was within 0.8% and none was disjoint from the `-O3` arm's samples. What it +/// buys on a dense generated bundle is large — #8418 measured a further +/// 346.7 MiB off a 4,743-module executable beyond the structural wins. +/// +/// So the trade is compile time (about +21% on that bundle) against shipped +/// binary size, with runtime speed unaffected. Smaller artifacts for every user +/// are worth a developer-side build cost, so unset means enabled. +/// +/// `PERRY_LL_SIZE_OPT=0` (or `off`/`false`/`no`) restores `-O3` for bisection or +/// for a build that would rather have the compile time back. fn size_optimization_requested(value: Option<&str>) -> bool { - value - .map(str::trim) - .map(str::to_ascii_lowercase) - .is_some_and(|value| matches!(value.as_str(), "1" | "true" | "on" | "yes")) + match value.map(str::trim).map(str::to_ascii_lowercase) { + None => true, + Some(value) => !matches!(value.as_str(), "0" | "false" | "off" | "no"), + } } fn build_clang_compile_plan( @@ -330,9 +343,10 @@ fn build_clang_compile_plan( cpu_tuning_arg_for(requested_cpu.as_deref(), target_triple, &effective_target); let stderr_remarks_path = PathBuf::from(format!("{}.clang-stderr", obj_path.display())); - // Perry defaults to speed-optimized native output. Generated-bundle users - // can explicitly trade runtime speed for artifact size with - // PERRY_LL_SIZE_OPT; there is no module-size-driven policy change. + // Perry defaults to SIZE-optimized native output: `-Os` measured no runtime + // cost on the benchmark corpus (see `size_optimization_requested`), and it + // materially shrinks dense generated bundles. `PERRY_LL_SIZE_OPT=0` restores + // `-O3`. There is no module-size-driven policy change. let size_opt = env::var("PERRY_LL_SIZE_OPT").ok(); let opt_flag = if size_optimization_requested(size_opt.as_deref()) { "-Os" diff --git a/crates/perry-codegen/src/linker_tests.rs b/crates/perry-codegen/src/linker_tests.rs index e4021bafe1..dfbd0a6209 100644 --- a/crates/perry-codegen/src/linker_tests.rs +++ b/crates/perry-codegen/src/linker_tests.rs @@ -167,8 +167,9 @@ fn compile_plan_records_effective_target_and_native_tuning() { false, ); assert!(plan.clang_args.contains(&"-fno-math-errno".to_string())); - // Native compilation defaults to speed-optimized -O3. - assert!(plan.clang_args.contains(&"-O3".to_string())); + // Native compilation defaults to size-optimized -Os (see + // `size_optimization_requested`); `PERRY_LL_SIZE_OPT=0` restores -O3. + assert!(plan.clang_args.contains(&"-Os".to_string())); assert!(plan.clang_args.contains(&"-target".to_string())); assert!(plan.analysis_clang_args.contains(&"-target".to_string())); // Apple aarch64 pins `apple-m1` rather than `native`: the decision to emit @@ -185,11 +186,15 @@ fn compile_plan_records_effective_target_and_native_tuning() { } #[test] -fn compile_plan_defaults_to_o3() { +fn compile_plan_defaults_to_os() { // Module size is deliberately absent from the compile plan: Perry's // runtime optimization contract does not change for large generated IR. // Scalability is handled by codegen-unit partitioning and structured // outlining before LLVM sees the function bodies. + // + // The default optimization level is `-Os`: measured on the quiet bench mini, + // `-Os` costs no runtime speed on the benchmark corpus while materially + // shrinking dense generated bundles. `PERRY_LL_SIZE_OPT=0` restores `-O3`. let plan = build_clang_compile_plan( PathBuf::from("clang"), PathBuf::from("/tmp/input.ll"), @@ -198,20 +203,24 @@ fn compile_plan_defaults_to_o3() { false, false, ); - assert!(plan.clang_args.contains(&"-O3".to_string())); - assert!(!plan.clang_args.contains(&"-Os".to_string())); + assert!(plan.clang_args.contains(&"-Os".to_string())); + assert!(!plan.clang_args.contains(&"-O3".to_string())); assert!(!plan.clang_args.contains(&"-O0".to_string())); } #[test] -fn size_optimization_flag_is_explicit_and_truthy() { - for enabled in ["1", "true", "TRUE", " on ", "yes"] { +fn size_optimization_is_on_unless_explicitly_disabled() { + // Unset means enabled — the default flipped once `-Os` was measured to cost + // no runtime speed on the benchmark corpus. + assert!(size_optimization_requested(None)); + for enabled in ["1", "true", "TRUE", " on ", "yes", "", "anything-else"] { assert!(size_optimization_requested(Some(enabled)), "{enabled}"); } - for disabled in ["", "0", "false", "off", "no", "anything-else"] { + // Only an explicit negative restores `-O3`, for bisection or to buy back + // compile time. + for disabled in ["0", "false", "off", "no", "OFF", " 0 "] { assert!(!size_optimization_requested(Some(disabled)), "{disabled}"); } - assert!(!size_optimization_requested(None)); } #[test]