Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
> ⚠️ We discourage the use of `process(input).first` / `process(input)[0]` because it silently drops potential additional documents
> Please use `process_one` if you are expecting only one JSON doc, e.g. in API payloads, because it emits on_warning if it finds multiple docs.

## 1.2.4 (2026-07-01)

RSpec tests: 1,268 → 1,282

### Bug Fixes

- **Portable builds by default — fixes the "Illegal instruction" crash on mixed-hardware fleets.** The C extension was compiled with `-march=native` on every platform except Apple Silicon, baking in the build host's CPU instructions (e.g. AVX-512). A binary built on one machine could then crash with `Illegal instruction` when run on a CPU lacking those instructions — common when the build host differs from the run host (CI/build servers, Docker images, mixed-hardware fleets). The C extension is now built **portable** by default (no host-specific instructions).

### New Features

- **`SMARTER_JSON_PERFORMANCE` build option** (`portable` default, `tuned`, or `max`) to opt into host-tuned or host-specific instructions at install time. See the [README](README.md#cpu-optimization-smarter_json_performance) / [Introduction](docs/_introduction.md#build-time-performance-tuning-smarter_json_performance) for details.

## 1.2.3 (2026-06-28)

RSpec tests: 1,167 → 1,268
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@ gem install smarter_json

The C extension is built on install and used automatically. On platforms where it can't build, the pure-Ruby implementation runs instead and produces identical results.

### CPU Optimization (`SMARTER_JSON_PERFORMANCE`)

The C extension is compiled when the gem is installed. By default it is built **portable**: it uses no CPU-specific instructions, so a binary built on one machine runs on any other CPU of the same architecture. Set `SMARTER_JSON_PERFORMANCE` at install time to trade portability for speed:

| Level | Flags added | Portable? | Use when |
|----------------------|-------------------------------------------|----------------------------------|---------------------------------------|
| `portable` (default) | none | Yes, any CPU of the arch | Build host may differ from run host |
| `tuned` | `-mtune=native` | Yes, instruction scheduling only | Build and run hosts share a microarch |
| `max` | `-march=native`, or `-mcpu=native` on ARM | No, host instruction optimization| Build host and run host are the same |

`max` enables host-specific instructions, so a binary built with it can crash with `Illegal instruction` if it later runs on a CPU that lacks them (for example, built on an AVX-512 machine and run on one without). `tuned` only changes instruction scheduling, never the instruction set, so it stays portable. Every flag is probed against your compiler at build time and skipped if unsupported, so an unavailable flag never breaks the build.

```bash
SMARTER_JSON_PERFORMANCE=tuned gem install smarter_json # portable, tuned for this machine's microarchitecture
SMARTER_JSON_PERFORMANCE=max gem install smarter_json # fastest, NOT portable — only when you build on the machine you run on
SMARTER_JSON_PERFORMANCE=tuned bundle install # same, under Bundler
```

For a fixed baseline instead of `native` (e.g. a portable-but-newer instruction set), pass flags directly via `CFLAGS`, which the build also honors: `CFLAGS="-march=x86-64-v2" gem install smarter_json`.

## Usage

Pass a String of JSON content or an IO; you get back the extracted data. The same call handles strict JSON, JSON5, and HJSON-style config — there are no modes or flags.
Expand Down
22 changes: 22 additions & 0 deletions docs/_introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ It raises only on genuinely unreadable input (unterminated string, mismatched br

Both the C extension and the pure-Ruby engine are **iterative, not recursive** — they track nesting on an explicit, heap-allocated stack rather than the call stack. So deeply nested input **cannot overflow the call stack or segfault**: nesting is bounded only by available memory, the same posture as Oj (the stdlib `json` caps at 100). The trade-off: there is currently **no fixed nesting or input-size limit**, so size-limit untrusted input upstream.

## Build-Time Performance Tuning (`SMARTER_JSON_PERFORMANCE`)

The C extension is compiled when the gem is installed. By default it is built **portable**: it uses no CPU-specific instructions, so a binary compiled on one machine runs on any other CPU of the same architecture. This matters whenever the machine that builds the gem differs from the machine that runs it — a CI or build server, a Docker image moved between hosts, or a mixed-hardware fleet. A build that bakes in instructions the run host lacks (such as AVX-512) would otherwise crash with `Illegal instruction`.

Set `SMARTER_JSON_PERFORMANCE` at install time to trade portability for speed:

| Level | Flags added | Portable? | Use when |
|----------------------|-------------------------------------------|----------------------------------|---------------------------------------|
| `portable` (default) | none | Yes, any CPU of the arch | Build host may differ from run host |
| `tuned` | `-mtune=native` | Yes, instruction scheduling only | Build and run hosts share a microarch |
| `max` | `-march=native`, or `-mcpu=native` on ARM | No, host instruction optimization| Build host and run host are the same |

`tuned` only changes instruction scheduling, never the instruction set, so it stays portable — and it pays off when the build and run hosts share a microarchitecture (the same chip, or a fleet of identical instances). `max` enables host-specific instructions and is the fastest, but a binary built with it can crash on a different CPU. Every flag is probed against your compiler at build time and skipped if unsupported, so an unavailable flag never breaks the build.

```bash
SMARTER_JSON_PERFORMANCE=tuned gem install smarter_json # portable, tuned for this machine's microarchitecture
SMARTER_JSON_PERFORMANCE=max gem install smarter_json # fastest, NOT portable — only when you build on the machine you run on
SMARTER_JSON_PERFORMANCE=tuned bundle install # same, under Bundler
```

For a fixed baseline instead of `native` (e.g. a portable-but-newer instruction set), pass flags directly via `CFLAGS`, which the build also honors: `CFLAGS="-march=x86-64-v2" gem install smarter_json`.

---------------

NEXT: [The Basic Read API](./basic_read_api.md) | UP: [README](../README.md)
55 changes: 55 additions & 0 deletions ext/smarter_json/cpu_flags.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module SmarterJSON
# Pure (mkmf-free) selection of CPU-optimization flags from the
# SMARTER_JSON_PERFORMANCE environment variable. Kept separate from extconf.rb
# so the logic can be unit-tested without invoking a compiler.
#
# Levels:
# portable - no host-specific flags. The binary runs on any CPU of the same
# architecture. The safe default: a binary built here will not
# crash with "Illegal instruction" on an older/different CPU.
# tuned - -mtune=native: tunes instruction scheduling for the build host's
# microarchitecture WITHOUT changing the instruction set, so the
# binary stays portable. A real win when build and run hosts share
# a microarchitecture (same chip or a homogeneous fleet).
# max - host-specific instructions: -march=native, or -mcpu=native on
# ARM/Clang where -march=native is rejected. Fastest, but NOT
# portable -- may crash on a CPU lacking the build host's
# instructions. Use only when build host and run host match.
#
# `accepts` is a predicate (in the real build, a wrapper over mkmf's
# try_compile) returning true when the compiler accepts a given flag; each
# candidate is probed so an unsupported flag is skipped rather than breaking
# the build.
module CpuFlags
LEVELS = %w[portable tuned max].freeze

# Candidate flags per level, in preference order. The first one the compiler
# accepts wins. `max` degrades march -> mcpu -> mtune; tuned only ever
# considers -mtune=native (never an instruction-set flag).
CANDIDATES = {
'portable' => [].freeze,
'tuned' => ['-mtune=native'].freeze,
'max' => ['-march=native', '-mcpu=native', '-mtune=native'].freeze,
}.freeze

# Returns a Hash: { level: String, flags: Array<String>, warning: String|nil }.
def self.select(raw_level, accepts:)
level, warning = normalize(raw_level)
chosen = CANDIDATES[level].find { |flag| accepts.call(flag) }
{ level: level, flags: chosen ? [chosen] : [], warning: warning }
end

# Normalizes the env value to a known level. Unknown values fall back to
# 'portable' (a typo can then only ever be slower, never non-portable) and
# return a warning naming the bad value and the fallback.
def self.normalize(raw_level)
value = raw_level.to_s.strip.downcase
return ['portable', nil] if value.empty?
return [value, nil] if LEVELS.include?(value)

['portable', "SMARTER_JSON_PERFORMANCE=#{raw_level.inspect} is not one of #{LEVELS.join('|')}; using 'portable'."]
end
end
end
29 changes: 25 additions & 4 deletions ext/smarter_json/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,39 @@

require "mkmf"
require "rbconfig"
require_relative "cpu_flags"

# Ruby sometimes ships CFLAGS with "-g -O3"; drop the debug half so the
# extension is built optimized, not with debug info.
if RbConfig::MAKEFILE_CONFIG["CFLAGS"].include?("-g -O3")
RbConfig::MAKEFILE_CONFIG["CFLAGS"] = RbConfig::MAKEFILE_CONFIG["CFLAGS"].sub("-g -O3", "-O3 $(cflags)")
end

# Probe whether the compiler accepts a flag by compiling a trivial program with
# it. Lets us skip flags the toolchain rejects (e.g. -march=native on Clang/ARM,
# or GCC-only flags on MSVC) instead of breaking the build. Replaces the old
# RUBY_PLATFORM string guesses: ask the actual compiler, don't infer from the OS.
def compiler_accepts?(flag)
try_compile("int main(void){return 0;}", flag)
end

optflags = "-O3 -flto -fomit-frame-pointer -DNDEBUG".dup
# -march=native is skipped on arm64-darwin (Apple clang already targets the host).
optflags << " -march=native" unless RUBY_PLATFORM.start_with?("arm64-darwin")
# -fno-semantic-interposition: GCC/Clang only (not MSVC).
optflags << " -fno-semantic-interposition" unless RUBY_PLATFORM.include?("mswin")

# CPU optimization level, set via SMARTER_JSON_PERFORMANCE (default: portable).
# See cpu_flags.rb for the full description of each level.
#
# portable (default) - no host-specific flags; runs on any CPU of the same arch.
# tuned - -mtune=native; host scheduling tuning, still portable.
# max - host instruction set (-march/-mcpu native); fastest, but
# NOT portable -- may crash on a CPU lacking those instructions.
cpu = SmarterJSON::CpuFlags.select(ENV["SMARTER_JSON_PERFORMANCE"], accepts: method(:compiler_accepts?))
warn(cpu[:warning]) if cpu[:warning]
cpu[:flags].each { |flag| optflags << " #{flag}" }
puts("SmarterJSON performance level: #{cpu[:level]} -- optflags: #{optflags}")

# -fno-semantic-interposition: GCC/Clang only (not MSVC). Allows intra-library
# calls to bypass the PLT on Linux and enables more aggressive LTO inlining.
optflags << " -fno-semantic-interposition" if compiler_accepts?("-fno-semantic-interposition")

CONFIG["optflags"] = optflags
CONFIG["debugflags"] = ""
Expand Down
2 changes: 1 addition & 1 deletion lib/smarter_json/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module SmarterJSON
VERSION = "1.2.3"
VERSION = "1.2.4"
end
Comment thread
tilo marked this conversation as resolved.
96 changes: 96 additions & 0 deletions spec/cpu_flags_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# frozen_string_literal: true

require_relative "../ext/smarter_json/cpu_flags"

# Contract for the pure (mkmf-free) CPU-optimization flag selector used by
# ext/smarter_json/extconf.rb. It maps SMARTER_JSON_PERFORMANCE to the list of
# host flags to append, asking the supplied `accepts` predicate (which wraps the
# compiler probe in the real build) whether each candidate flag compiles.
#
# Safety contract: the default (and any unknown value) must NEVER add a
# host-specific instruction-set flag (-march/-mcpu native), because that is what
# makes a binary crash with "Illegal instruction" on a CPU that lacks the build
# host's instructions.
RSpec.describe SmarterJSON::CpuFlags do
let(:accepts_all) { ->(_flag) { true } }
let(:accepts_none) { ->(_flag) { false } }

def select(level, accepts: accepts_all)
described_class.select(level, accepts: accepts)
end

describe "portable (the default)" do
it "defaults to portable when the value is nil" do
result = select(nil)
expect(result[:level]).to eq "portable"
expect(result[:flags]).to eq []
expect(result[:warning]).to be_nil
end

it "defaults to portable when the value is empty" do
expect(select("")[:level]).to eq "portable"
end

it "adds no host flags even when the compiler would accept every flag" do
expect(select("portable", accepts: accepts_all)[:flags]).to eq []
end

it "is case-insensitive" do
expect(select("PORTABLE")[:level]).to eq "portable"
end

it "ignores surrounding whitespace" do
expect(select(" portable ")[:level]).to eq "portable"
end
end

describe "tuned" do
it "adds -mtune=native when the compiler accepts it" do
expect(select("tuned", accepts: accepts_all)[:flags]).to eq ["-mtune=native"]
end

it "adds nothing when the compiler rejects -mtune=native (e.g. MSVC)" do
expect(select("tuned", accepts: accepts_none)[:flags]).to eq []
end

it "never adds an instruction-set flag (-march/-mcpu)" do
flags = select("tuned", accepts: accepts_all)[:flags]
expect(flags).not_to include("-march=native")
expect(flags).not_to include("-mcpu=native")
end
end

describe "max" do
it "prefers -march=native when the compiler accepts it" do
expect(select("max", accepts: accepts_all)[:flags]).to eq ["-march=native"]
end

it "falls back to -mcpu=native when -march=native is rejected (e.g. Clang on ARM)" do
accepts = ->(flag) { flag != "-march=native" }
expect(select("max", accepts: accepts)[:flags]).to eq ["-mcpu=native"]
end

it "falls back to -mtune=native when both -march and -mcpu are rejected" do
accepts = ->(flag) { flag == "-mtune=native" }
expect(select("max", accepts: accepts)[:flags]).to eq ["-mtune=native"]
end

it "adds nothing when the compiler rejects every candidate (e.g. MSVC)" do
expect(select("max", accepts: accepts_none)[:flags]).to eq []
end
end

describe "unknown / typo values" do
it "falls back to portable so a typo can only ever be slower, never non-portable" do
result = select("fast")
expect(result[:level]).to eq "portable"
expect(result[:flags]).to eq []
end

it "warns, naming the bad value and the level it fell back to" do
warning = select("fast")[:warning]
expect(warning).to include("fast")
expect(warning).to include("portable")
end
end
end
Loading