Skip to content

Running a set of simple benchmarks, the Perry-compiled binary's RSS grew to **1432MB** — 4.5x more than Node #6010

Description

@zaaack

What happened

Running a set of simple benchmarks, the Perry-compiled binary's RSS grew to 1432MB — 4.5x more than Node (315MB) and 3.7x more than tsx (386MB). Heap usage peaked at 319MB vs 90MB (node) / 166MB (tsx).

More concerning: RSS kept growing monotonically across benchmarks even when heap was stable, suggesting either a leak or an unbounded arena allocator.

What you expected

A natively compiled binary should use less or similar memory to a JIT runtime, not 4.5x more. The gap between heapUsed and RSS (319MB heap → 1432MB RSS) also seems abnormally large.

Minimal reproduction

const ITERATIONS = 100;
const N = 100_000;

function bench(name: string, fn: () => void, iters: number) {
  for (let i = 0; i < Math.min(10, Math.floor(iters / 10)); i++) fn();
  const start = performance.now();
  for (let i = 0; i < iters; i++) fn();
  const mem = process.memoryUsage();
  console.log(`[${name}] rss=${(mem.rss / 1024 / 1024).toFixed(0)}MB | heap=${(mem.heapUsed / 1024 / 1024).toFixed(0)}MB`);
}

bench('array-sort-100k', () => {
  const arr = new Array<number>(N);
  for (let i = 0; i < N; i++) arr[i] = Math.random();
  arr.sort((a, b) => a - b);
}, ITERATIONS);

bench('filter-map-reduce-100k', () => {
  const arr = new Array<number>(N);
  for (let i = 0; i < N; i++) arr[i] = i;
  arr.filter(x => x % 2 === 0).map(x => x * x).reduce((acc, x) => acc + x, 0);
}, ITERATIONS * 2);

bench('map-set-get-100k', () => {
  const m = new Map<number, number>();
  for (let i = 0; i < N; i++) m.set(i, i * 2);
  let sum = 0;
  for (let i = 0; i < N; i++) sum += m.get(i) ?? 0;
  return sum;
}, ITERATIONS * 2);

bench('set-intersection-50k', () => {
  const half = N / 2;
  const a = new Set<number>();
  const b = new Set<number>();
  for (let i = 0; i < half; i++) { a.add(i); b.add(i + half); }
  const intersection = new Set<number>();
  for (const x of a) { if (b.has(x)) intersection.add(x); }
  return intersection.size;
}, ITERATIONS);

bench('string-concat-10k', () => {
  let s = '';
  for (let i = 0; i < 10_000; i++) s += 'a' + i.toString();
}, ITERATIONS);

const obj = { a: 1, b: 'foo', c: [1, 2, 3], d: { x: true } };
const jsonStr = JSON.stringify(obj);
bench('json-stringify-100', () => { for (let i = 0; i < 100; i++) JSON.stringify(obj); }, ITERATIONS * 10);
bench('json-parse-100', () => { for (let i = 0; i < 100; i++) JSON.parse(jsonStr); }, ITERATIONS * 10);

Command you ran:

perry compile benchmark.ts -o dist/benchmark-perry
./dist/benchmark-perry

Environment

  • Perry version: 0.5.1220
  • Host OS: linux
  • Target: native
  • Installed via: (from path /usr/local/bin/perry)

Diagnostic output

Perry RSS progression across benchmarks (cumulative, same process):

[array-sort-100k]      rss=39MB    heap=22MB
[filter-map-reduce]    rss=61MB    heap=16MB    ← heap drops but RSS grows
[map-set-get]          rss=821MB   heap=16MB    ← huge RSS jump
[set-intersection]     rss=1138MB  heap=16MB    ← heap flat, RSS +300MB
[string-concat]        rss=1223MB  heap=106MB
[json-stringify]       rss=1396MB  heap=287MB
[json-parse]           rss=1433MB  heap=319MB

Node.js baseline (same binary, same workload):

RSS 峰值: 315MB | Heap 峰值: 90MB

tsx baseline:

RSS 峰值: 386MB | Heap 峰值: 166MB

Anything else

Key observations:

  1. RSS spikes from 61MB → 821MB after map-set-get-100k (Map with 100k entries, ITERATIONS=200). This is suspicious.
  2. Heap stays at 16MB during the spike — the large RSS isn't reflected in heapUsed, suggesting allocations outside the GC-managed heap (direct native allocs, arenas, or mmap'd regions not tracked by heap metrics).
  3. RSS never decreases across the process lifetime — no page reclamation even after temporary allocations should have been freed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions