This commit is contained in:
2025-12-28 19:50:27 -06:00
parent 5f471ee003
commit c432bc211f
4 changed files with 493 additions and 32 deletions

211
bench.ce
View File

@@ -15,10 +15,12 @@ var target_bench = null // null = all benchmarks, otherwise specific bench file
var all_pkgs = false var all_pkgs = false
// Benchmark configuration // Benchmark configuration
def WARMUP_ITERATIONS = 5 def WARMUP_BATCHES = 3
def MIN_ITERATIONS = 10 def SAMPLES = 11 // Number of timing samples to collect
def MAX_ITERATIONS = 1000 def TARGET_SAMPLE_NS = 20000000 // 20ms per sample (fast mode)
def TARGET_DURATION_NS = 1000000000 // 1 second target per benchmark def MIN_SAMPLE_NS = 2000000 // 2ms minimum sample duration
def MIN_BATCH_SIZE = 1
def MAX_BATCH_SIZE = 100000000 // 100M iterations max per batch
// Statistical functions // Statistical functions
function median(arr) { function median(arr) {
@@ -191,43 +193,184 @@ function collect_benches(package_name, specific_bench) {
return bench_files return bench_files
} }
// Calibrate batch size for a benchmark
function calibrate_batch_size(bench_fn, is_batch) {
if (!is_batch) return 1
var n = MIN_BATCH_SIZE
var dt = 0
// Find a batch size that takes at least MIN_SAMPLE_NS
while (n < MAX_BATCH_SIZE) {
// Ensure n is a valid number before calling
if (typeof n != 'number' || n < 1) {
n = 1
break
}
var start = os.now()
bench_fn(n)
dt = os.now() - start
if (dt >= MIN_SAMPLE_NS) break
// Double the batch size
var new_n = n * 2
// Check if multiplication produced a valid number
if (typeof new_n != 'number' || new_n > MAX_BATCH_SIZE) {
n = MAX_BATCH_SIZE
break
}
n = new_n
}
// Adjust to target sample duration
if (dt > 0 && dt < TARGET_SAMPLE_NS && typeof n == 'number' && typeof dt == 'number') {
var calc = n * TARGET_SAMPLE_NS / dt
if (typeof calc == 'number' && calc > 0) {
var target_n = number.floor(calc)
// Check if floor returned a valid number
if (typeof target_n == 'number' && target_n > 0) {
if (target_n > MAX_BATCH_SIZE) target_n = MAX_BATCH_SIZE
if (target_n < MIN_BATCH_SIZE) target_n = MIN_BATCH_SIZE
n = target_n
}
}
}
// Safety check - ensure we always return a valid batch size
if (typeof n != 'number' || n < 1) {
n = 1
}
return n
}
// Run a single benchmark function // Run a single benchmark function
function run_single_bench(bench_fn, bench_name) { function run_single_bench(bench_fn, bench_name) {
var timings = [] var timings_per_op = []
// Detect benchmark format:
// 1. Object with { setup, run, teardown } - structured format
// 2. Function that accepts (n) - batch format
// 3. Function that accepts () - legacy format
var is_structured = typeof bench_fn == 'object' && bench_fn.run
var is_batch = false
var batch_size = 1
var setup_fn = null
var run_fn = null
var teardown_fn = null
if (is_structured) {
setup_fn = bench_fn.setup || function() { return null }
run_fn = bench_fn.run
teardown_fn = bench_fn.teardown || function(state) {}
// Check if run function accepts batch size
try {
var test_state = setup_fn()
run_fn(1, test_state)
is_batch = true
if (teardown_fn) teardown_fn(test_state)
} catch (e) {
is_batch = false
}
// Create wrapper for calibration
var calibrate_fn = function(n) {
var state = setup_fn()
run_fn(n, state)
if (teardown_fn) teardown_fn(state)
}
batch_size = calibrate_batch_size(calibrate_fn, is_batch)
// Safety check for structured benchmarks
if (typeof batch_size != 'number' || batch_size < 1) {
batch_size = 1
}
} else {
// Simple function format
try {
bench_fn(1)
is_batch = true
} catch (e) {
is_batch = false
}
batch_size = calibrate_batch_size(bench_fn, is_batch)
}
// Safety check - ensure batch_size is valid
if (!batch_size || batch_size < 1) {
batch_size = 1
}
// Warmup phase // Warmup phase
for (var i = 0; i < WARMUP_ITERATIONS; i++) { for (var i = 0; i < WARMUP_BATCHES; i++) {
bench_fn() // Ensure batch_size is valid before warmup
if (typeof batch_size != 'number' || batch_size < 1) {
log.console(`WARNING: batch_size became ${typeof batch_size} = ${batch_size}, resetting to 1`)
batch_size = 1
}
if (is_structured) {
var state = setup_fn()
if (is_batch) {
run_fn(batch_size, state)
} else {
run_fn(state)
}
if (teardown_fn) teardown_fn(state)
} else {
if (is_batch) {
bench_fn(batch_size)
} else {
bench_fn()
}
}
} }
// Determine how many iterations to run // Measurement phase - collect SAMPLES timing samples
var test_start = os.now() for (var i = 0; i < SAMPLES; i++) {
bench_fn() // Double-check batch_size is valid (should never happen, but defensive)
var test_duration = os.now() - test_start if (typeof batch_size != 'number' || batch_size < 1) {
batch_size = 1
}
var iterations = MIN_ITERATIONS if (is_structured) {
if (test_duration > 0) { var state = setup_fn()
iterations = number.floor(TARGET_DURATION_NS / test_duration) var start = os.now()
if (iterations < MIN_ITERATIONS) iterations = MIN_ITERATIONS if (is_batch) {
if (iterations > MAX_ITERATIONS) iterations = MAX_ITERATIONS run_fn(batch_size, state)
} } else {
run_fn(state)
}
var duration = os.now() - start
if (teardown_fn) teardown_fn(state)
// Measurement phase var ns_per_op = is_batch ? duration / batch_size : duration
for (var i = 0; i < iterations; i++) { timings_per_op.push(ns_per_op)
var start = os.now() } else {
bench_fn() var start = os.now()
var duration = os.now() - start if (is_batch) {
timings.push(duration) bench_fn(batch_size)
} else {
bench_fn()
}
var duration = os.now() - start
var ns_per_op = is_batch ? duration / batch_size : duration
timings_per_op.push(ns_per_op)
}
} }
// Calculate statistics // Calculate statistics
var mean_ns = mean(timings) var mean_ns = mean(timings_per_op)
var median_ns = median(timings) var median_ns = median(timings_per_op)
var min_ns = min_val(timings) var min_ns = min_val(timings_per_op)
var max_ns = max_val(timings) var max_ns = max_val(timings_per_op)
var stddev_ns = stddev(timings, mean_ns) var stddev_ns = stddev(timings_per_op, mean_ns)
var p95_ns = percentile(timings, 95) var p95_ns = percentile(timings_per_op, 95)
var p99_ns = percentile(timings, 99) var p99_ns = percentile(timings_per_op, 99)
// Calculate ops/s from median // Calculate ops/s from median
var ops_per_sec = 0 var ops_per_sec = 0
@@ -237,7 +380,8 @@ function run_single_bench(bench_fn, bench_name) {
return { return {
name: bench_name, name: bench_name,
iterations: iterations, batch_size: batch_size,
samples: SAMPLES,
mean_ns: number.round(mean_ns), mean_ns: number.round(mean_ns),
median_ns: number.round(median_ns), median_ns: number.round(median_ns),
min_ns: number.round(min_ns), min_ns: number.round(min_ns),
@@ -318,6 +462,9 @@ function run_benchmarks(package_name, specific_bench) {
log.console(` ${result.name}`) log.console(` ${result.name}`)
log.console(` ${format_ns(result.median_ns)}/op ${format_ops(result.ops_per_sec)}`) log.console(` ${format_ns(result.median_ns)}/op ${format_ops(result.ops_per_sec)}`)
log.console(` min: ${format_ns(result.min_ns)} max: ${format_ns(result.max_ns)} stddev: ${format_ns(result.stddev_ns)}`) log.console(` min: ${format_ns(result.min_ns)} max: ${format_ns(result.max_ns)} stddev: ${format_ns(result.stddev_ns)}`)
if (result.batch_size > 1) {
log.console(` batch: ${result.batch_size} samples: ${result.samples}`)
}
} catch (e) { } catch (e) {
log.console(` ERROR ${b.name}: ${e}`) log.console(` ERROR ${b.name}: ${e}`)
log.error(e) log.error(e)
@@ -417,7 +564,7 @@ Total benchmarks: ${total_benches}
if (b.error) continue if (b.error) continue
txt_report += `\n${pkg_res.package}::${b.name}\n` txt_report += `\n${pkg_res.package}::${b.name}\n`
txt_report += ` iterations: ${b.iterations}\n` txt_report += ` batch_size: ${b.batch_size} samples: ${b.samples}\n`
txt_report += ` median: ${format_ns(b.median_ns)}/op\n` txt_report += ` median: ${format_ns(b.median_ns)}/op\n`
txt_report += ` mean: ${format_ns(b.mean_ns)}/op\n` txt_report += ` mean: ${format_ns(b.mean_ns)}/op\n`
txt_report += ` min: ${format_ns(b.min_ns)}\n` txt_report += ` min: ${format_ns(b.min_ns)}\n`

261
benches/micro_ops.cm Normal file
View File

@@ -0,0 +1,261 @@
// micro_ops.bench.ce (or .cm depending on your convention)
// Note: We use a function-local sink in each benchmark to avoid cross-contamination
function blackhole(sink, x) {
// Prevent dead-code elimination
return (sink + (x | 0)) | 0
}
function make_obj_xy(x, y) {
return { x, y }
}
function make_obj_yx(x, y) {
// Different insertion order to force a different shape in many engines
return { y, x }
}
function make_shapes(n) {
var out = []
for (var i = 0; i < n; i++) {
var o = { a: i }
o[`p${i}`] = i
out.push(o)
}
return out
}
function make_packed_array(n) {
var a = []
for (var i = 0; i < n; i++) a.push(i)
return a
}
function make_holey_array(n) {
var a = []
for (var i = 0; i < n; i += 2) a[i] = i
return a
}
return {
// 0) Baseline loop cost
loop_empty: function(n) {
var sink = 0
for (var i = 0; i < n; i++) {}
return blackhole(sink, n)
},
// 1) Numeric pipelines
i32_add: function(n) {
var sink = 0
var x = 1
for (var i = 0; i < n; i++) x = (x + 3) | 0
return blackhole(sink, x)
},
f64_add: function(n) {
var sink = 0
var x = 1.0
for (var i = 0; i < n; i++) x = x + 3.14159
return blackhole(sink, x | 0)
},
mixed_add: function(n) {
var sink = 0
var x = 1
for (var i = 0; i < n; i++) x = x + 0.25
return blackhole(sink, x | 0)
},
bit_ops: function(n) {
var sink = 0
var x = 0x12345678
for (var i = 0; i < n; i++) x = ((x << 5) ^ (x >>> 3)) | 0
return blackhole(sink, x)
},
overflow_path: function(n) {
var sink = 0
var x = 0x70000000
for (var i = 0; i < n; i++) x = (x + 0x10000000) | 0
return blackhole(sink, x)
},
// 2) Branching
branch_predictable: function(n) {
var sink = 0
var x = 0
for (var i = 0; i < n; i++) {
if ((i & 7) != 0) x++
else x += 2
}
return blackhole(sink, x)
},
branch_alternating: function(n) {
var sink = 0
var x = 0
for (var i = 0; i < n; i++) {
if ((i & 1) == 0) x++
else x += 2
}
return blackhole(sink, x)
},
// 3) Calls
call_direct: function(n) {
var sink = 0
function f(a) { return (a + 1) | 0 }
var x = 0
for (var i = 0; i < n; i++) x = f(x)
return blackhole(sink, x)
},
call_indirect: function(n) {
var sink = 0
function f(a) { return (a + 1) | 0 }
var g = f
var x = 0
for (var i = 0; i < n; i++) x = g(x)
return blackhole(sink, x)
},
call_closure: function(n) {
var sink = 0
function make_adder(k) {
return function(a) { return (a + k) | 0 }
}
var add3 = make_adder(3)
var x = 0
for (var i = 0; i < n; i++) x = add3(x)
return blackhole(sink, x)
},
// 4) Object props (ICs / shapes)
prop_read_mono: function(n) {
var sink = 0
var o = make_obj_xy(1, 2)
var x = 0
for (var i = 0; i < n; i++) x = (x + o.x) | 0
return blackhole(sink, x)
},
prop_read_poly_2: function(n) {
var sink = 0
var a = make_obj_xy(1, 2)
var b = make_obj_yx(1, 2)
var x = 0
for (var i = 0; i < n; i++) {
var o = (i & 1) == 0 ? a : b
x = (x + o.x) | 0
}
return blackhole(sink, x)
},
prop_read_mega: function(n) {
var sink = 0
var objs = make_shapes(32)
var x = 0
for (var i = 0; i < n; i++) {
var o = objs[i & 31]
x = (x + o.a) | 0
}
return blackhole(sink, x)
},
prop_write_mono: function(n) {
var sink = 0
var o = make_obj_xy(1, 2)
for (var i = 0; i < n; i++) o.x = (o.x + 1) | 0
return blackhole(sink, o.x)
},
// 5) Arrays
array_read_packed: function(n) {
var sink = 0
var a = make_packed_array(1024)
var x = 0
for (var i = 0; i < n; i++) x = (x + a[i & 1023]) | 0
return blackhole(sink, x)
},
array_write_packed: function(n) {
var sink = 0
var a = make_packed_array(1024)
for (var i = 0; i < n; i++) a[i & 1023] = i
return blackhole(sink, a[17] | 0)
},
array_read_holey: function(n) {
var sink = 0
var a = make_holey_array(2048)
var x = 0
for (var i = 0; i < n; i++) {
var v = a[(i & 2047)]
// If "missing" is a special value in your language, this stresses that path too
if (v) x = (x + v) | 0
}
return blackhole(sink, x)
},
array_push_steady: function(n) {
var sink = 0
var x = 0
for (var j = 0; j < n; j++) {
var a = []
for (var i = 0; i < 256; i++) a.push(i)
x = (x + a.length) | 0
}
return blackhole(sink, x)
},
// 6) Strings
string_concat_small: function(n) {
var sink = 0
var x = 0
for (var j = 0; j < n; j++) {
var s = ""
for (var i = 0; i < 16; i++) s = s + "x"
x = (x + s.length) | 0
}
return blackhole(sink, x)
},
// 7) Allocation / GC pressure
alloc_tiny_objects: function(n) {
var sink = 0
var x = 0
for (var i = 0; i < n; i++) {
var o = { a: i, b: i + 1, c: i + 2 }
x = (x + o.b) | 0
}
return blackhole(sink, x)
},
alloc_linked_list: function(n) {
var sink = 0
var head = null
for (var i = 0; i < n; i++) head = { v: i, next: head }
var x = 0
var p = head
while (p) {
x = (x + p.v) | 0
p = p.next
}
return blackhole(sink, x)
},
// 8) meme-specific (adapt these to your exact semantics)
meme_clone_read: function(n) {
// If meme(obj) clones like Object.create / prototypal clone, this hits it hard.
// Replace with your exact meme call form.
var sink = 0
var base = { x: 1, y: 2 }
var x = 0
for (var i = 0; i < n; i++) {
var o = meme(base)
x = (x + o.x) | 0
}
return blackhole(sink, x)
}
}

View File

@@ -177,6 +177,7 @@ array.reduce = function(arr, fn, initial, reverse) {
array.for = function(arr, fn, reverse, exit) { array.for = function(arr, fn, reverse, exit) {
if (!_isArray(arr)) return null if (!_isArray(arr)) return null
if (arr.length == 0) return null
if (typeof fn != 'function') return null if (typeof fn != 'function') return null
if (reverse == true) { if (reverse == true) {

View File

@@ -14357,6 +14357,15 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
v2 &= 0x1f; v2 &= 0x1f;
sp[-2] = JS_NewInt32(ctx, v1 << v2); sp[-2] = JS_NewInt32(ctx, v1 << v2);
sp--; sp--;
} else if (JS_VALUE_IS_BOTH_FLOAT(op1, op2) ||
(JS_VALUE_GET_TAG(op1) == JS_TAG_INT && JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op2))) ||
(JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op1)) && JS_VALUE_GET_TAG(op2) == JS_TAG_INT)) {
uint32_t v1, v2;
v1 = JS_VALUE_GET_TAG(op1) == JS_TAG_INT ? JS_VALUE_GET_INT(op1) : (int32_t)JS_VALUE_GET_FLOAT64(op1);
v2 = JS_VALUE_GET_TAG(op2) == JS_TAG_INT ? JS_VALUE_GET_INT(op2) : (int32_t)JS_VALUE_GET_FLOAT64(op2);
v2 &= 0x1f;
sp[-2] = JS_NewInt32(ctx, v1 << v2);
sp--;
} else { } else {
sp[-2] = JS_NULL; sp[-2] = JS_NULL;
sp--; sp--;
@@ -14376,6 +14385,15 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
(uint32_t)JS_VALUE_GET_INT(op1) >> (uint32_t)JS_VALUE_GET_INT(op1) >>
v2); v2);
sp--; sp--;
} else if (JS_VALUE_IS_BOTH_FLOAT(op1, op2) ||
(JS_VALUE_GET_TAG(op1) == JS_TAG_INT && JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op2))) ||
(JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op1)) && JS_VALUE_GET_TAG(op2) == JS_TAG_INT)) {
uint32_t v1, v2;
v1 = JS_VALUE_GET_TAG(op1) == JS_TAG_INT ? JS_VALUE_GET_INT(op1) : (int32_t)JS_VALUE_GET_FLOAT64(op1);
v2 = JS_VALUE_GET_TAG(op2) == JS_TAG_INT ? JS_VALUE_GET_INT(op2) : (int32_t)JS_VALUE_GET_FLOAT64(op2);
v2 &= 0x1f;
sp[-2] = JS_NewUint32(ctx, v1 >> v2);
sp--;
} else { } else {
sp[-2] = JS_NULL; sp[-2] = JS_NULL;
sp--; sp--;
@@ -14394,6 +14412,16 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
sp[-2] = JS_NewInt32(ctx, sp[-2] = JS_NewInt32(ctx,
(int)JS_VALUE_GET_INT(op1) >> v2); (int)JS_VALUE_GET_INT(op1) >> v2);
sp--; sp--;
} else if (JS_VALUE_IS_BOTH_FLOAT(op1, op2) ||
(JS_VALUE_GET_TAG(op1) == JS_TAG_INT && JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op2))) ||
(JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op1)) && JS_VALUE_GET_TAG(op2) == JS_TAG_INT)) {
int32_t v1;
uint32_t v2;
v1 = JS_VALUE_GET_TAG(op1) == JS_TAG_INT ? JS_VALUE_GET_INT(op1) : (int32_t)JS_VALUE_GET_FLOAT64(op1);
v2 = JS_VALUE_GET_TAG(op2) == JS_TAG_INT ? JS_VALUE_GET_INT(op2) : (int32_t)JS_VALUE_GET_FLOAT64(op2);
v2 &= 0x1f;
sp[-2] = JS_NewInt32(ctx, v1 >> v2);
sp--;
} else { } else {
sp[-2] = JS_NULL; sp[-2] = JS_NULL;
sp--; sp--;
@@ -14410,6 +14438,14 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JS_VALUE_GET_INT(op1) & JS_VALUE_GET_INT(op1) &
JS_VALUE_GET_INT(op2)); JS_VALUE_GET_INT(op2));
sp--; sp--;
} else if (JS_VALUE_IS_BOTH_FLOAT(op1, op2) ||
(JS_VALUE_GET_TAG(op1) == JS_TAG_INT && JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op2))) ||
(JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op1)) && JS_VALUE_GET_TAG(op2) == JS_TAG_INT)) {
int32_t v1, v2;
v1 = JS_VALUE_GET_TAG(op1) == JS_TAG_INT ? JS_VALUE_GET_INT(op1) : (int32_t)JS_VALUE_GET_FLOAT64(op1);
v2 = JS_VALUE_GET_TAG(op2) == JS_TAG_INT ? JS_VALUE_GET_INT(op2) : (int32_t)JS_VALUE_GET_FLOAT64(op2);
sp[-2] = JS_NewInt32(ctx, v1 & v2);
sp--;
} else { } else {
sp[-2] = JS_NULL; sp[-2] = JS_NULL;
sp--; sp--;
@@ -14426,6 +14462,14 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JS_VALUE_GET_INT(op1) | JS_VALUE_GET_INT(op1) |
JS_VALUE_GET_INT(op2)); JS_VALUE_GET_INT(op2));
sp--; sp--;
} else if (JS_VALUE_IS_BOTH_FLOAT(op1, op2) ||
(JS_VALUE_GET_TAG(op1) == JS_TAG_INT && JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op2))) ||
(JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op1)) && JS_VALUE_GET_TAG(op2) == JS_TAG_INT)) {
int32_t v1, v2;
v1 = JS_VALUE_GET_TAG(op1) == JS_TAG_INT ? JS_VALUE_GET_INT(op1) : (int32_t)JS_VALUE_GET_FLOAT64(op1);
v2 = JS_VALUE_GET_TAG(op2) == JS_TAG_INT ? JS_VALUE_GET_INT(op2) : (int32_t)JS_VALUE_GET_FLOAT64(op2);
sp[-2] = JS_NewInt32(ctx, v1 | v2);
sp--;
} else { } else {
sp[-2] = JS_NULL; sp[-2] = JS_NULL;
sp--; sp--;
@@ -14442,6 +14486,14 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JS_VALUE_GET_INT(op1) ^ JS_VALUE_GET_INT(op1) ^
JS_VALUE_GET_INT(op2)); JS_VALUE_GET_INT(op2));
sp--; sp--;
} else if (JS_VALUE_IS_BOTH_FLOAT(op1, op2) ||
(JS_VALUE_GET_TAG(op1) == JS_TAG_INT && JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op2))) ||
(JS_TAG_IS_FLOAT64(JS_VALUE_GET_TAG(op1)) && JS_VALUE_GET_TAG(op2) == JS_TAG_INT)) {
int32_t v1, v2;
v1 = JS_VALUE_GET_TAG(op1) == JS_TAG_INT ? JS_VALUE_GET_INT(op1) : (int32_t)JS_VALUE_GET_FLOAT64(op1);
v2 = JS_VALUE_GET_TAG(op2) == JS_TAG_INT ? JS_VALUE_GET_INT(op2) : (int32_t)JS_VALUE_GET_FLOAT64(op2);
sp[-2] = JS_NewInt32(ctx, v1 ^ v2);
sp--;
} else { } else {
sp[-2] = JS_NULL; sp[-2] = JS_NULL;
sp--; sp--;