WASM SIMD

New to WASM in TJS? Start with the WASM Quick Start.

Process 4 floats per instruction. No setup, no toolchain.

/#
## SIMD: Single Instruction, Multiple Data

SIMD processes 4 float values per instruction — a 4x throughput
improvement for vectorized math. TJS provides SIMD via `f32x4_*`
intrinsics that compile directly to WASM SIMD opcodes.

### Available Intrinsics

| Intrinsic | Operation |
|-----------|-----------|
| `f32x4_load(arr, offset)` | Load 4 floats from array |
| `f32x4_store(arr, offset, vec)` | Store 4 floats to array |
| `f32x4_splat(value)` | Fill all 4 lanes with one value |
| `f32x4_add(a, b)` | Add 4 pairs |
| `f32x4_sub(a, b)` | Subtract 4 pairs |
| `f32x4_mul(a, b)` | Multiply 4 pairs |
| `f32x4_div(a, b)` | Divide 4 pairs |
| `f32x4_neg(a)` | Negate 4 values |
| `f32x4_sqrt(a)` | Square root of 4 values |
| `f32x4_extract_lane(vec, lane)` | Get one float (0-3) |
| `f32x4_replace_lane(vec, lane, val)` | Set one float |
#/

// --- Scale an array by a constant (SIMD: 4 elements per step) ---

function scale(! arr: Float32Array, len: 0, factor: 0.0) {
  wasm {
    let s = f32x4_splat(factor)
    for (let i = 0; i < len; i += 4) {
      let off = i * 4
      let v = f32x4_load(arr, off)
      f32x4_store(arr, off, f32x4_mul(v, s))
    }
  } fallback {
    for (let i = 0; i < len; i++) arr[i] *= factor
  }
}

// --- Dot product (sum of element-wise products) ---

function dot(! a: Float32Array, b: Float32Array, len: 0):! 0.0 {
  return wasm {
    let acc = f32x4_splat(0.0)
    for (let i = 0; i < len; i += 4) {
      let off = i * 4
      let va = f32x4_load(a, off)
      let vb = f32x4_load(b, off)
      acc = f32x4_add(acc, f32x4_mul(va, vb))
    }
    // Sum the 4 lanes
    return f32x4_extract_lane(acc, 0)
      + f32x4_extract_lane(acc, 1)
      + f32x4_extract_lane(acc, 2)
      + f32x4_extract_lane(acc, 3)
  } fallback {
    let sum = 0
    for (let i = 0; i < len; i++) sum += a[i] * b[i]
    return sum
  }
}

// --- Demo ---

const SIZE = 1024

// Create test arrays
const arr = new Float32Array(SIZE)
const a = new Float32Array(SIZE)
const b = new Float32Array(SIZE)

for (let i = 0; i < SIZE; i++) {
  arr[i] = i + 1
  a[i] = 1.0
  b[i] = 2.0
}

// Scale
scale(arr, SIZE, 0.5)
console.log('scale([1..1024], 0.5) first 4:', arr[0], arr[1], arr[2], arr[3])

// Dot product: 1.0 * 2.0 * 1024 = 2048
const d = dot(a, b, SIZE)
console.log('dot([1,1,...], [2,2,...], 1024):', d)

// Benchmark
// Measure per-op time by running until at least minMs of wall clock has elapsed,
// then dividing. Timing a single sub-millisecond pass does not work: Firefox clamps
// `performance.now()` to ~1ms for fingerprinting resistance, so a fast result reads
// as 0.00ms — and on hardware faster than whatever the iteration count was tuned for,
// a fixed count drifts back under the clock all by itself.
function timePerOp(fn, minMs = 25) {
  let iters = 0
  const start = performance.now()
  do {
    fn()
    iters++
  } while (performance.now() - start < minMs)
  return (performance.now() - start) / iters
}

const perOp = timePerOp(() => scale(arr, SIZE, 1.001))
console.log(`scale on ${SIZE} floats: ${(perOp * 1000).toFixed(1)}us per op`)
console.log(`  (${(SIZE / perOp / 1000).toFixed(1)} million floats/sec)`)