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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,30 @@ freed under it**, which is the same trade Swift makes with ARC.
See **[docs/memory-models.md](docs/memory-models.md)** for which shapes leak, which do not, and
what to do about it.

### Performance

The ray tracer benchmark in [tslang/benchmarks/raytrace](tslang/benchmarks/raytrace/README.md)
(`raytrace.ts` at 512x512, median of 7 runs, 2026-10-21) against Rust and C++ ports of the same
code, each written twice: with plain value vectors and colors, and with every object
reference-counted on the heap the way tslang allocates them:

| build | time | peak memory | vs C++ values (clang -O3) |
| --- | --- | --- | --- |
| `-mm=gc` | 1298 ms | 6 MB | 26x slower |
| `-mm=rc` | 599 ms | 4 MB | 12x slower |
| `-mm=none` | 703 ms | 845 MB | 14x slower |
| `-mm=own` | does not compile yet | | |
| C++, values (clang / MSVC) | 51 / 69 ms | | |
| Rust, values (`Copy` vectors and colors) | 55 ms | 4 MB | 1.1x slower |
| Rust, every object in an `Rc` | 492 ms | 4 MB | 9.7x slower |
| C++, every object in a `shared_ptr` (clang / MSVC) | 830 / 899 ms | | 16x slower |

Almost all of the gap is allocation: every vector and color the program computes is a heap object.
Rust that allocates the same way is only 1.2x faster than `-mm=rc`, and C++ with `shared_ptr`
(atomic counts) is 1.4x slower than it. `-mm=none` is slower than
`-mm=rc` because it never frees and keeps touching fresh pages. Stack promotion makes no measurable
difference here yet: the hot allocations are returned from functions or captured by closures.

### `-mm=own`

`-mm=own` works the way Rust's borrow checker does. Every heap block has exactly one owner: a
Expand Down
3 changes: 3 additions & 0 deletions tslang/benchmarks/raytrace/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
out/
rust/target/
rust/Cargo.lock
56 changes: 56 additions & 0 deletions tslang/benchmarks/raytrace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Ray tracer: memory models vs Rust and C++

`rtbench.ts` is `test/tester/tests/raytrace.ts` at 512x512 with a pixel checksum (see its header).
`rust/` and `cpp/` each hold two ports of the same algorithm:

- `rust/src/bin/idiomatic.rs`, `cpp/idiomatic.cpp`: `Vector`/`Color` are plain values, things are
`Box<dyn Thing>` / `std::unique_ptr<Thing>`, surfaces hold function pointers. This is what a
Rust or C++ programmer would write.
- `rust/src/bin/boxed.rs`, `cpp/shared.cpp`: every `Vector`, `Color`, `Ray` and `Intersection` is
a fresh reference-counted heap object (`Rc` / `std::make_shared`), things are shared too, and
surface members are `Rc<dyn Fn>` / `std::function`. This is roughly the allocation pattern
tslang produces, so the gap between a language's two ports is the cost of allocation alone.
`Rc` counts are plain integers; `shared_ptr` counts are atomic.

Run (needs a Release tslang and cargo; clang++ on PATH and Visual Studio are used when present):

```
pwsh tslang/benchmarks/raytrace/bench.ps1
```

- tslang: `--emit=exe --opt --opt_level=3 --no-default-lib --mm=<model>` (`--no-default-lib`
because the file declares its own `sqrt`/`floor`/`pow`/`fabs`)
- Rust: `opt-level=3`, `lto="fat"`, `codegen-units=1`, `panic="abort"`
- C++: `clang++ -std=c++20 -O3`, and `cl /std:c++20 /O2 /EHsc /MT`

## Results 2026-10-21

main 49b5f6bb, Release, Windows 11 x64; Rust 1.99.0, clang 21.1.4, MSVC from Visual Studio 18;
median of 7 runs after a warm-up. Every build prints the same checksum.

| Build | median | peak working set | vs C++ idiomatic (clang) |
|---|---|---|---|
| tslang gc | 1298 ms | 6 MB | 26x |
| tslang gc, `--no-stack-promotion` | 1341 ms | | |
| tslang rc | 599 ms | 4 MB | 12x |
| tslang rc, `--no-stack-promotion` | 600 ms | | |
| tslang none | 703 ms | 845 MB | 14x |
| tslang none, `--no-stack-promotion` | 727 ms | | |
| tslang own | does not compile | | |
| Rust idiomatic | 55 ms | 4 MB | 1.1x |
| Rust Rc-boxed | 492 ms | 4 MB | 9.7x |
| C++ idiomatic (clang) | 51 ms | | 1x |
| C++ idiomatic (MSVC) | 69 ms | | 1.4x |
| C++ shared_ptr (clang) | 830 ms | | 16x |
| C++ shared_ptr (MSVC) | 899 ms | | 18x |

- Almost all of the gap to Rust and C++ is allocation: their heap-allocating ports are 9-16x
slower than their idiomatic ones. rc is 1.2x slower than Rust with `Rc`, and 1.4x faster than
C++ with `shared_ptr` (atomic counts, `std::function` calls).
- `none` never frees, so it pays for fresh pages and ends up slower than `rc`.
- Stack promotion has no measurable effect here. `--emit=mlir-affine --stack-promotion-report`
under rc keeps every hot-path allocation on the heap: `Vector.*`/`Color.*` results are
"returned", the `Ray`/`Intersection` literals are "converted" (to the interface) or
"recursive", and the `addLight`/`getPoint` closures are "counted-capture".
- `-mm=own` reports about 44 errors: the program shares the `Surfaces` globals, keeps owned values
in object literals, and aliases `Color.black` from two statics.
58 changes: 58 additions & 0 deletions tslang/benchmarks/raytrace/bench.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Builds rtbench.ts under every memory model (with and without stack promotion) and the two Rust
# ports, checks that they all print the same checksum, then times each one: a warm-up run and
# $Runs timed runs, reporting min/median/max wall time.
#
# pwsh tslang/benchmarks/raytrace/bench.ps1 [-Tslang <path to tslang.exe>] [-Runs 7]
param(
[string]$Tslang = "$PSScriptRoot\..\..\..\__build\tslang\windows-msbuild-2026-release\bin\tslang.exe",
[int]$Runs = 7
)

$ErrorActionPreference = "Stop"
$out = Join-Path $PSScriptRoot "out"
New-Item -ItemType Directory -Force $out | Out-Null

$bins = @()
foreach ($mm in "gc", "rc", "none", "own") {
foreach ($sp in $false, $true) {
$name = if ($sp) { "tslang $mm (no SP)" } else { "tslang $mm" }
$exe = Join-Path $out ("rtb_$mm" + $(if ($sp) { "_nsp" } else { "" }) + ".exe")
$flags = @("--emit=exe", "--opt", "--opt_level=3", "--no-default-lib", "--mm=$mm")
if ($sp) { $flags += "--no-stack-promotion" }
& $Tslang @flags "$PSScriptRoot\rtbench.ts" -o $exe 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) { "{0,-22} does not compile (exit {1})" -f $name, $LASTEXITCODE; continue }
$bins += @{ n = $name; p = $exe }
}
}

Push-Location "$PSScriptRoot\rust"
try { cargo build --release --quiet; if ($LASTEXITCODE -ne 0) { throw "cargo build failed" } } finally { Pop-Location }
$bins += @{ n = "rust idiomatic"; p = "$PSScriptRoot\rust\target\release\idiomatic.exe" }
$bins += @{ n = "rust Rc-boxed"; p = "$PSScriptRoot\rust\target\release\boxed.exe" }

# C++: clang++ -O3 from PATH, and MSVC /O2 through the newest Visual Studio's vcvars64
$vs = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -prerelease -property installationPath
$vcvars = "$vs\VC\Auxiliary\Build\vcvars64.bat"
foreach ($f in "idiomatic", "shared") {
$src = "$PSScriptRoot\cpp\$f.cpp"
if (Get-Command clang++ -ErrorAction SilentlyContinue) {
$exe = "$out\cpp_${f}_clang.exe"
clang++ -std=c++20 -O3 -o $exe $src
if ($LASTEXITCODE -eq 0) { $bins += @{ n = "c++ $f (clang)"; p = $exe } }
}
if (Test-Path $vcvars) {
$exe = "$out\cpp_${f}_msvc.exe"
cmd /c "`"$vcvars`" >nul && cl /nologo /std:c++20 /O2 /EHsc /MT /Fe:`"$exe`" /Fo:`"$out\\`" `"$src`"" | Out-Null
if ($LASTEXITCODE -eq 0) { $bins += @{ n = "c++ $f (msvc)"; p = $exe } }
}
}

foreach ($b in $bins) {
$text = (& $b.p) -join " "
$ok = $text -match "38837369|low digits: 369"
$times = 1..$Runs | ForEach-Object {
$sw = [Diagnostics.Stopwatch]::StartNew(); & $b.p | Out-Null; $sw.Stop(); $sw.Elapsed.TotalMilliseconds
} | Sort-Object
"{0,-22} min {1,8:N1} ms median {2,8:N1} ms max {3,8:N1} ms checksum {4}" -f `
$b.n, $times[0], $times[[int][math]::Floor($Runs / 2)], $times[-1], $(if ($ok) { "ok" } else { "MISMATCH: $text" })
}
230 changes: 230 additions & 0 deletions tslang/benchmarks/raytrace/cpp/idiomatic.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
// Idiomatic C++ port of rtbench.ts: Vector/Color are plain value structs, things are
// std::unique_ptr<Thing> with virtual methods, surfaces hold function pointers.
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <memory>
#include <optional>
#include <vector>

struct Vector {
double x, y, z;
static Vector times(double k, Vector v) { return {k * v.x, k * v.y, k * v.z}; }
static Vector minus(Vector a, Vector b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
static Vector plus(Vector a, Vector b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
static double dot(Vector a, Vector b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
static double mag(Vector v) { return std::sqrt(v.x * v.x + v.y * v.y + v.z * v.z); }
static Vector norm(Vector v) {
double m = mag(v);
double div = (m == 0) ? std::numeric_limits<double>::infinity() : 1.0 / m;
return times(div, v);
}
static Vector cross(Vector a, Vector b) {
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
}
};

struct Color {
double r, g, b;
static Color scale(double k, Color v) { return {k * v.r, k * v.g, k * v.b}; }
static Color plus(Color a, Color b) { return {a.r + b.r, a.g + b.g, a.b + b.b}; }
static Color times(Color a, Color b) { return {a.r * b.r, a.g * b.g, a.b * b.b}; }
};

constexpr Color white{1.0, 1.0, 1.0};
constexpr Color grey{0.5, 0.5, 0.5};
constexpr Color black{0.0, 0.0, 0.0};
constexpr Color background = black;
constexpr Color defaultColor = black;

struct DrawingColor { double r, g, b; };

static DrawingColor toDrawingColor(Color c) {
auto legalize = [](double d) { return d > 1 ? 1.0 : d; };
return {std::floor(legalize(c.r) * 255), std::floor(legalize(c.g) * 255), std::floor(legalize(c.b) * 255)};
}

struct Camera {
Vector pos, forward, right, up;
Camera(Vector pos, Vector lookAt) : pos(pos) {
Vector down{0.0, -1.0, 0.0};
forward = Vector::norm(Vector::minus(lookAt, pos));
right = Vector::times(1.5, Vector::norm(Vector::cross(forward, down)));
up = Vector::times(1.5, Vector::norm(Vector::cross(forward, right)));
}
};

struct Ray { Vector start, dir; };

struct Thing;
struct Intersection { const Thing *thing; Ray ray; double dist; };

struct Surface {
Color (*diffuse)(Vector);
Color (*specular)(Vector);
double (*reflect)(Vector);
double roughness;
};

struct Thing {
virtual ~Thing() = default;
virtual std::optional<Intersection> intersect(const Ray &ray) const = 0;
virtual Vector normal(Vector pos) const = 0;
const Surface *surface;
explicit Thing(const Surface *surface) : surface(surface) {}
};

struct Light { Vector pos; Color color; };

struct Scene {
std::vector<std::unique_ptr<Thing>> things;
std::vector<Light> lights;
Camera camera;
};

struct Sphere : Thing {
Vector center;
double radius2;
Sphere(Vector center, double radius, const Surface *surface) : Thing(surface), center(center), radius2(radius * radius) {}
Vector normal(Vector pos) const override { return Vector::norm(Vector::minus(pos, center)); }
std::optional<Intersection> intersect(const Ray &ray) const override {
Vector eo = Vector::minus(center, ray.start);
double v = Vector::dot(eo, ray.dir);
double dist = 0;
if (v >= 0) {
double disc = radius2 - (Vector::dot(eo, eo) - v * v);
if (disc >= 0) dist = v - std::sqrt(disc);
}
if (dist == 0) return std::nullopt;
return Intersection{this, ray, dist};
}
};

struct Plane : Thing {
Vector norm;
double offset;
Plane(Vector norm, double offset, const Surface *surface) : Thing(surface), norm(norm), offset(offset) {}
Vector normal(Vector) const override { return norm; }
std::optional<Intersection> intersect(const Ray &ray) const override {
double denom = Vector::dot(norm, ray.dir);
if (denom > 0) return std::nullopt;
double dist = (Vector::dot(norm, ray.start) + offset) / (-denom);
return Intersection{this, ray, dist};
}
};

static bool odd(Vector pos) { return std::fmod(std::floor(pos.z) + std::floor(pos.x), 2) != 0; }

const Surface shiny{
[](Vector) { return white; },
[](Vector) { return grey; },
[](Vector) { return 0.7; },
250.0};

const Surface checkerboard{
[](Vector pos) { return odd(pos) ? white : black; },
[](Vector) { return white; },
[](Vector pos) { return odd(pos) ? 0.1 : 0.7; },
150.0};

struct RayTracer {
int maxDepth = 5;

std::optional<Intersection> intersections(const Ray &ray, const Scene &scene) const {
double closest = std::numeric_limits<double>::infinity();
std::optional<Intersection> closestInter;
for (auto &thing : scene.things) {
auto inter = thing->intersect(ray);
if (inter && inter->dist < closest) {
closestInter = inter;
closest = inter->dist;
}
}
return closestInter;
}

std::optional<double> testRay(const Ray &ray, const Scene &scene) const {
auto isect = intersections(ray, scene);
if (isect) return isect->dist;
return std::nullopt;
}

Color traceRay(const Ray &ray, const Scene &scene, int depth) const {
auto isect = intersections(ray, scene);
if (!isect) return background;
return shade(*isect, scene, depth);
}

Color shade(const Intersection &isect, const Scene &scene, int depth) const {
Vector d = isect.ray.dir;
Vector pos = Vector::plus(Vector::times(isect.dist, d), isect.ray.start);
Vector normal = isect.thing->normal(pos);
Vector reflectDir = Vector::minus(d, Vector::times(2, Vector::times(Vector::dot(normal, d), normal)));
Color naturalColor = Color::plus(background, getNaturalColor(isect.thing, pos, normal, reflectDir, scene));
Color reflectedColor = (depth >= maxDepth) ? grey : getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth);
return Color::plus(naturalColor, reflectedColor);
}

Color getReflectionColor(const Thing *thing, Vector pos, Vector, Vector rd, const Scene &scene, int depth) const {
return Color::scale(thing->surface->reflect(pos), traceRay({pos, rd}, scene, depth + 1));
}

Color getNaturalColor(const Thing *thing, Vector pos, Vector norm, Vector rd, const Scene &scene) const {
auto addLight = [&](Color col, const Light &light) {
Vector ldis = Vector::minus(light.pos, pos);
Vector livec = Vector::norm(ldis);
auto neatIsect = testRay({pos, livec}, scene);
bool isInShadow = neatIsect ? (*neatIsect <= Vector::mag(ldis)) : false;
if (isInShadow) return col;
double illum = Vector::dot(livec, norm);
Color lcolor = (illum > 0) ? Color::scale(illum, light.color) : defaultColor;
double specular = Vector::dot(livec, Vector::norm(rd));
Color scolor = (specular > 0) ? Color::scale(std::pow(specular, thing->surface->roughness), light.color) : defaultColor;
return Color::plus(col, Color::plus(Color::times(thing->surface->diffuse(pos), lcolor),
Color::times(thing->surface->specular(pos), scolor)));
};
Color col = defaultColor;
for (auto &light : scene.lights) col = addLight(col, light);
return col;
}

double render(const Scene &scene, double screenWidth, double screenHeight) const {
auto getPoint = [&](double x, double y, const Camera &camera) {
auto recenterX = [&](double x) { return (x - (screenWidth / 2.0)) / 2.0 / screenWidth; };
auto recenterY = [&](double y) { return -(y - (screenHeight / 2.0)) / 2.0 / screenHeight; };
return Vector::norm(Vector::plus(camera.forward, Vector::plus(Vector::times(recenterX(x), camera.right), Vector::times(recenterY(y), camera.up))));
};
double sum = 0;
for (double y = 0; y < screenHeight; y++) {
for (double x = 0; x < screenWidth; x++) {
Color color = traceRay({scene.camera.pos, getPoint(x, y, scene.camera)}, scene, 0);
DrawingColor c = toDrawingColor(color);
sum += c.r + c.g + c.b;
}
}
return sum;
}
};

static Scene defaultScene() {
Scene scene{{}, {}, Camera({3.0, 2.0, 4.0}, {-1.0, 0.5, 0.0})};
scene.things.push_back(std::make_unique<Plane>(Vector{0.0, 1.0, 0.0}, 0.0, &checkerboard));
scene.things.push_back(std::make_unique<Sphere>(Vector{0.0, 1.0, -0.25}, 1.0, &shiny));
scene.things.push_back(std::make_unique<Sphere>(Vector{-6, 3.0, -5.0}, 1.0, &shiny));
scene.things.push_back(std::make_unique<Sphere>(Vector{-1.0, 0.5, 1.5}, 0.5, &shiny));
scene.lights = {{{-2.0, 2.5, 0.0}, {0.49, 0.07, 0.07}},
{{1.5, 2.5, 1.5}, {0.07, 0.07, 0.49}},
{{1.5, 2.5, -1.5}, {0.07, 0.49, 0.071}},
{{0.0, 3.5, 0.0}, {0.21, 0.21, 0.35}}};
return scene;
}

int main(int argc, char **argv) {
std::puts("start...");
double size = argc > 1 ? std::atof(argv[1]) : 512;
RayTracer rayTracer;
double sum = rayTracer.render(defaultScene(), size, size);
std::printf("checksum: %.0f\n", sum);
std::puts("done.");
}
Loading
Loading