Skip to content

Build with -O3 -fno-strict-overflow; drop -march=native - #8

Merged
jeremy merged 1 commit into
mainfrom
optimization-flags
Aug 3, 2026
Merged

Build with -O3 -fno-strict-overflow; drop -march=native#8
jeremy merged 1 commit into
mainfrom
optimization-flags

Conversation

@jeremy

@jeremy jeremy commented Aug 2, 2026

Copy link
Copy Markdown
Member

Steelmanning -march=native turned up the opposite of what we assumed, plus a bigger problem underneath it.

-march=native is slower

~12% behind plain -O3 on Zen 4 (Ryzen 9 7950X), worst on exactly the numeric loops it should help. Two independent runs, 21 interleaved passes, best-of:

benchmark -O3 -march=native -O3 effect of native
float_math 0.1037 0.0858 21% slower
int_math 0.0820 0.0709 16% slower
alloc_gc 0.0274 0.0252 9% slower
array_sort_map 0.0221 0.0206 8% slower
hash_churn 0.0186 0.0180 3% slower
method_call 0.0091 0.0091
regexp_scan 0.0268 0.0266
string_build 0.0212 0.0215 2% faster
aggregate 0.311 0.278 12% slower

Microbenchmarks are the friendliest case -march=native gets — tight numeric loops are where wider instructions should pay off. It loses there, so there's no reason to expect a win on real work. Consistency and performance point the same way.

The bigger find: four definitions were building unoptimized

Leaving RUBY_CFLAGS empty doesn't mean "use configure's default". ruby-build exports it as CFLAGS, which supersedes configure's own optflags in the compile line, so an empty value builds at -O0:

1.8.7 -O0-O3 2.3.8 -O0-O3
method_call 3.1× 2.6×
int_math 2.7× 3.4×
string_build 2.5× 3.5×
hash_churn 2.4× 3.3×
array_ops 2.2× 2.9×

RbConfig::CONFIG["optflags"] still reports -O3 in that state, which is why it went unnoticed — the flag string lies, the timings don't. Unintended fallout from b719789, which made RUBY_CFLAGS="" unconditional where it had previously only been set under clang.

Two hazards that -O0 was masking

Turning optimization on activates machinery -O0 silently disables. Neither is caught by "does it compile and run":

-fno-strict-overflow is mandatory, not caution. These sources assume signed overflow wraps; GCC treats that as UB and exploits it from -O2 up. 1.8.7 built at -O3 without it compiles cleanly, runs, loads every stdlib — and evaluates 2**64 to 0, typed Fixnum. Silent wrong arithmetic. -O2 breaks it too, so this isn't -O3 aggressiveness. The flag costs nothing measurable: within noise on 2.7.8, two of five benchmarks nominally faster with it.

1.9.3 caps _FORTIFY_SOURCE at 2 under GCC. Ubuntu's GCC raises it to 3 whenever optimizing, and level 3's object-size inference aborts the build with *** buffer overflow detected *** while running the freshly built miniruby. Not a hardening regression — fortify does nothing without optimization, so at -O0 there was none at all; level 2 is strictly more checking than today. Scoped to 1.9.3, since the rest build fine at 3. I tested disabling fortify outright first and preferred this.

test/build now asserts arithmetic

The existing checks would have passed a Ruby computing 2**64 == 0. Verified the new one earns its place by building the unsafe variant deliberately — it cleared ruby --version, openssl, digest and zlib before the assertion caught 2**64 wrong: 0.

Failures also dump ruby-build's own log and strip curl's progress meter. Several definitions send compiler output to that log rather than stdout, and the container is --rm, so diagnosing the 1.9.3 abort meant reproducing it by hand — the reported tail was entirely progress bars.

Verification

Full matrix green, 12/12 across Arch and Ubuntu Noble, including the ubuntu-noble 1.9.3-p551 case that this change initially broke.

Benchmarks are microbenchmarks on one machine (Zen 4, GCC 16 on Arch / GCC 13.3 on Ubuntu). They measure interpreter CPU paths; real dev work is diluted by I/O and GC, so expect the -O0-O3 win to land smaller than 2-3× in a Rails suite — but in the same direction, and the correctness findings are independent of workload.

Steelmanning -march=native turned up the opposite of what we assumed, plus a
bigger problem underneath it.

-march=native is slower. ~12% behind plain -O3 on Zen 4, worst on the numeric
loops it should help most: -16% integer, -21% float. Two runs, 21 interleaved
passes, best-of. Microbenchmarks are the friendliest case it gets, so there's
no reason to expect a win on real work. Dropped.

The bigger find: the other four definitions were building unoptimized. Leaving
RUBY_CFLAGS empty doesn't mean "use configure's default" — ruby-build exports
it as CFLAGS, which supersedes configure's optflags in the compile line, so an
empty value is -O0. Measured 2.2-3.5x slower across method calls, integer math,
string building, hash churn and array ops. rbconfig still reports optflags: -O3
in that state, which is why it went unnoticed; the timings are what to trust.
Unintended fallout from b719789, which made RUBY_CFLAGS="" unconditional where
it had previously only been set under clang.

Turning optimization on activates two things -O0 was silently masking, and
neither is caught by "does it compile and run":

-fno-strict-overflow is mandatory, not caution. These sources assume signed
overflow wraps; GCC treats that as UB and exploits it from -O2 up. 1.8.7 built
at -O3 without it compiles clean, runs, loads every stdlib — and evaluates
2**64 to 0, typed Fixnum. Silent wrong arithmetic. Confirmed -O2 breaks it too,
and that the flag costs nothing measurable (within noise on 2.7.8, two of five
benchmarks nominally faster with it).

1.9.3 additionally caps _FORTIFY_SOURCE at 2 under GCC. Ubuntu's GCC raises it
to 3 whenever optimizing, and level 3's object-size inference aborts the build
with "*** buffer overflow detected ***" while running the freshly built
miniruby. Not a hardening regression: fortify does nothing without optimization,
so at -O0 there was none at all. Scoped to 1.9.3; the rest build fine at 3.

So test/build now asserts overflow arithmetic. The existing checks would have
passed a Ruby computing 2**64 == 0 — verified by building the unsafe variant and
watching it clear ruby --version, openssl, digest and zlib before the new
assertion caught it. Failures also dump ruby-build's own log now, and strip
curl's progress meter: several definitions send compiler output to that log
rather than stdout, and diagnosing the 1.9.3 abort meant reproducing it by hand
because the container took the log with it.

Full matrix green, 12/12 on Arch and Ubuntu Noble.
Copilot AI review requested due to automatic review settings August 2, 2026 23:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@jeremy

jeremy commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 9e0b58c63d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jeremy
jeremy merged commit fb80a75 into main Aug 3, 2026
1 of 2 checks passed
@jeremy
jeremy deleted the optimization-flags branch August 3, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants