Performance¶
How fast gorge plays Magic, measured against the other rules engines on the same machine, the same decks and one CPU core each. This page is the headline comparison for anyone choosing an engine to host games or to generate self-play data; Methodology says exactly how it was measured and how to rerun it, Engine speed covers gorge's own benchmark and profile, and Bots covers what gorge's bots cost and how well they play.
The short version (2026-10-20, one core, FDN limited decks)
- gorge plays a random-policy game in about 10 ms, about 40× as many games per core-second as XMage (89 against 2.4 games per core-second; 29× cheaper per turn).
- With each engine's own AI on both seats, a gorge game costs 7-9 ms, against about 1 s for Forge and XMage's default AI: 100-150× more games per core-second. The AIs are not the same kind of player (see below), so this is the price of a game with the bot each engine ships, not engine speed alone.
- dirty-dirac, the two-player kernel checked in lockstep against gorge, is another 15-20× faster than gorge (0.6 ms a game, 23 µs a turn), at the cost of playing only its generated card pools. Its rows were measured before the 2026-10-21 kernel redesign; see dirty-dirac's rows.
- mtg-kernel, a public Rust kernel with a few hundred cards, is about as fast as gorge on its home Burn mirror (174 against 118-143 random games per core-second) and 6-13× slower per turn on FDN pair A, the FDN pair it plays (details).
- Search: one MCTS simulation costs 57-108 µs on dirty-dirac with a heuristic leaf, 90-177 µs with a small network leaf, and 0.9-1.6 ms on gorge, at 25-1,000 simulations per decision; almost all of it is the rules engine (details).
- Memory: one game-playing process peaks at about 350 MB for gorge (the whole card corpus loaded), 0.9 GB for Forge and 1.2-1.3 GB for XMage with a 2 GB heap cap; mtg-kernel, with its cards compiled in, 11-16 MB.
The numbers¶
Each bar is the median of six runs (FDN pairs A and B, three reps each); the thin line through it spans the slowest and fastest run. Engines differ by more than three orders of magnitude, so the first two charts use a log scale: each grid line is ten times the one before. Hover a bar for its exact value and spread.
ad3777023, one pinned core per run (24/26/28), load average 4.4-15; scripts/xengine-bench.sh. Filled bars: random legal action; outlined bars: the engine's own AI.ad3777023, one pinned core per run (24/26/28), load average 4.4-15; scripts/xengine-bench.sh. Filled bars: random legal action; outlined bars: the engine's own AI.ad3777023, one pinned core per run (24/26/28), load average 4.4-15; scripts/xengine-bench.sh. Filled bars: random legal action; outlined bars: the engine's own AI.| Engine · policy | games / core-s | ms / game | µs / turn | µs / decision | turns / game | peak RSS (MB) |
|---|---|---|---|---|---|---|
| dirty-dirac · random | 1,722 (1,580–1,848) | 0.584 (0.541–0.633) | 23.3 (22.8–24.0) | 4.62 (4.54–4.75) | 25 | 356 |
| gorge · random (auto-pay) | 89.4 (85.4–93.9) | 11.2 (10.7–11.7) | 459 (443–473) | 19.3 (18.8–19.7) | 24 | 351 |
| gorge · random (manual) | 105 (102–108) | 9.56 (9.23–9.78) | 233 (232–236) | 8.49 (8.46–8.54) | 41 | 349 |
| XMage · random | 2.38 (2.16–2.58) | 420 (387–462) | 13,193 (12,774–13,751) | 499 (487–518) | 32 | 1,228 |
| dirty-dirac · hbot | 1,607 (1,439–1,763) | 0.628 (0.567–0.695) | 35.0 (31.8–38.6) | 6.35 (5.94–6.84) | 18 | 485 |
| gorge · bot (auto-pay) | 107 (102–112) | 9.36 (8.96–9.83) | 525 (507–544) | 21.3 (20.9–21.7) | 18 | 348 |
| gorge · bot (manual) | 140 (132–145) | 7.16 (6.88–7.58) | 400 (385–417) | 14.4 (14.0–14.9) | 18 | 355 |
| XMage · simple AI | 19.3 (18.6–19.7) | 51.9 (50.7–53.9) | 761 (743–791) | 48.3 (47.1–50.2) | 68 | 1,219 |
| XMage · mad AI | 0.947 (0.552–1.25) | 1,121 (801–1,813) | 62,343 (49,069–101,733) | 2,902 (2,292–4,782) | 17 | 1,288 |
| Forge · Forge AI | 1.02 (0.723–1.22) | 983 (823–1,383) | 54,513 (45,904–71,387) | n/a | 18 | 882 |
Per-pair detail (the summary table xengine-bench.sh prints)
| engine | policy | pair | reps | games/core-s | ms/game | µs/turn | µs/decision | turns/game | peak RSS MB | load1 | other busy |
|---|---|---|---|---|---|---|---|---|---|---|---|
| gorge | random-autopay | A | 3 | 92.3 (91.9-93.9) | 10.8 (10.7-10.9) | 471 (468-473) | 19.7 (19.5-19.7) | 22.9 | 351 | 7-7 | max 0% |
| gorge | random | A | 3 | 103 (102-103) | 9.74 (9.70-9.78) | 232 (232-233) | 8.50 (8.48-8.54) | 41.9 | 349 | 7-7 | max 0% |
| gorge | bot-autopay | A | 3 | 103 (102-103) | 9.70 (9.68-9.83) | 540 (538-544) | 21.5 (21.4-21.7) | 18.0 | 346 | 6-7 | max 0% |
| gorge | bot | A | 3 | 134 (132-136) | 7.45 (7.37-7.58) | 412 (410-417) | 14.7 (14.7-14.9) | 18.1 | 355 | 6-6 | max 0% |
| gorge | random-autopay | B | 3 | 86.8 (85.4-86.8) | 11.5 (11.5-11.7) | 445 (443-450) | 18.9 (18.8-19.0) | 26.0 | 347 | 6-6 | max 0% |
| gorge | random | B | 3 | 108 (106-108) | 9.28 (9.23-9.42) | 233 (233-236) | 8.46 (8.46-8.54) | 39.8 | 348 | 6-6 | max 0% |
| gorge | bot-autopay | B | 3 | 111 (110-112) | 9.02 (8.96-9.05) | 509 (507-512) | 21.0 (20.9-21.1) | 17.7 | 348 | 5-6 | max 0% |
| gorge | bot | B | 3 | 144 (144-145) | 6.95 (6.88-6.96) | 388 (385-390) | 14.0 (14.0-14.1) | 17.9 | 348 | 5-6 | max 0% |
| dirty-dirac | random | A | 3 | 1,848 (1,845-1,848) | 0.54 (0.54-0.54) | 22.9 (22.8-22.9) | 4.55 (4.54-4.56) | 23.7 | 361 | 5-6 | max 0% |
| dirty-dirac | hbot | A | 3 | 1,457 (1,439-1,462) | 0.69 (0.68-0.69) | 38.1 (38.1-38.6) | 6.76 (6.75-6.84) | 18.0 | 490 | 5-5 | max 0% |
| dirty-dirac | random | B | 3 | 1,587 (1,580-1,598) | 0.63 (0.63-0.63) | 23.8 (23.7-24.0) | 4.71 (4.69-4.75) | 26.4 | 356 | 5-5 | max 0% |
| dirty-dirac | hbot | B | 3 | 1,763 (1,751-1,763) | 0.57 (0.57-0.57) | 31.8 (31.8-31.9) | 5.95 (5.94-5.95) | 17.9 | 480 | 4-5 | max 0% |
| xmage | random | A | 3 | 2.50 (2.40-2.58) | 400 (387-416) | 13,063 (12,774-13,145) | 495 (487-496) | 30.6 | 1,228 | 4-5 | max 0% |
| xmage | simple | A | 3 | 19.6 (19.5-19.7) | 50.9 (50.7-51.3) | 747 (743-753) | 47.4 (47.1-47.8) | 68.2 | 1,218 | 5-5 | max 0% |
| xmage | mad | A | 3 | 1.22 (1.18-1.25) | 822 (801-850) | 51,169 (49,069-51,492) | 2,349 (2,292-2,391) | 16.3 | 1,286 | 5-12 | max 28% |
| xmage | random | B | 3 | 2.32 (2.16-2.36) | 430 (425-462) | 13,357 (13,242-13,751) | 502 (502-518) | 32.2 | 1,232 | 12-14 | max 7% |
| xmage | simple | B | 3 | 19.0 (18.6-19.1) | 52.7 (52.4-53.9) | 774 (770-791) | 49.1 (48.8-50.2) | 68.1 | 1,221 | 13-14 | max 0% |
| xmage | mad | B | 3 | 0.62 (0.55-0.72) | 1,610 (1,391-1,813) | 91,739 (73,194-101,733) | 4,255 (3,413-4,782) | 17.8 | 1,320 | 10-13 | max 0% |
| forge | ai | A | 3 | 0.81 (0.72-0.97) | 1,239 (1,035-1,383) | 67,548 (58,991-71,387) | n/a | 18.3 | 868 | 10-15 | max 3% |
| forge | ai | B | 3 | 1.16 (1.07-1.22) | 862 (823-932) | 46,518 (45,904-50,034) | n/a | 18.6 | 902 | 9-13 | max 0% |
What the comparison shows¶
Engine cost. The random rows measure each engine with a policy that costs almost nothing, so they are as close to pure engine cost as each engine allows. Per turn, dirty-dirac (23 µs) is about 20× cheaper than gorge with auto-pay (459 µs), which is about 29× cheaper than XMage (13.2 ms). gorge's manual-tap random row is cheaper per turn (233 µs) but plays twice as many turns per game, because a random player tapping lands one at a time wastes mana. Forge has no random player, so it has no row here.
AI cost. Within one engine, the AI rows show what the built-in bot adds.
gorge's heuristic bot ends games in 18 turns instead of 24-41, so a game with
two bots is cheaper than a random game, even though each turn costs a
little more (about 15% of a bot game's CPU is the bot itself, see
Engine speed). XMage's base ComputerPlayer
is the cheapest XMage policy per turn (0.76 ms), but its games drift to about
68 turns. XMage's default "mad" AI searches the game tree at every priority
and costs 50-100 ms a turn; Forge's AI costs 46-68 ms a turn.
Search. Search bots clone and step the engine many times per decision, so the per-operation costs matter more than per-game ones: gorge clones a mid-game state in 14 µs (28 µs with GC) and clones-and-steps in 39 µs, and its MCTS search runs about 1,000-1,100 simulations per core-second at 25-100 simulations a decision (see Engine speed and Search bots below, which compares it with dirty-dirac's search).
A second comparison: decks mtg-kernel plays¶
mtg-kernel is a public Rust rules kernel (MIT) built for reinforcement learning on a fixed card pool. It cannot play FDN pair B (it lacks four of its cards), so it is not in the tables above. This section adds it on the two workloads it does play, measured the same way (one pinned core per run, three reps, medians with min-max): FDN pair A, which is exactly mtg-kernel's two FDN fixture decks and which every engine here plays, and the Burn mirror, the mono-red Pauper deck from mtg-kernel's own pool, which every engine except dirty-dirac plays. mtg-kernel has no standalone built-in bot, so it has a random row only (why).
What differs about mtg-kernel
- Card pool. 452 card definitions at the measured commit: its Pauper decks plus 245 of FDN's 286 names. gorge plays about 94% of Forge's ~34,000-card corpus; XMage and Forge play every card.
- Its FDN cards are a build option. They sit behind the cargo feature
limited-fdn-fixtures, and that build plays Burn several times slower than the default build, so pair A uses the FDN build and Burn the default one. - Its random player is the one from its own benchmark: every priority window and every mana ability is a decision, as in gorge's manual-tap random row, which is the closest gorge row to compare with.
- Memory is not comparable: mtg-kernel compiles its few hundred cards into the binary (11-16 MB resident), while gorge, XMage and Forge load a card database of tens of thousands of cards.
FDN pair A, every engine¶
ad3777023, mtg-kernel 058d075e3, one pinned core per run (24/26/28); scripts/xengine-bench.sh. Filled bars: random legal action; outlined bars: the engine's own AI.| Engine · policy | games / core-s | ms / game | µs / turn | µs / decision | turns / game | peak RSS (MB) |
|---|---|---|---|---|---|---|
| dirty-dirac · random | 1,848 (1,845–1,848) | 0.541 (0.541–0.542) | 22.9 (22.8–22.9) | 4.55 (4.54–4.56) | 24 | 361 |
| gorge · random (auto-pay) | 92.3 (91.9–93.9) | 10.8 (10.7–10.9) | 471 (468–473) | 19.7 (19.5–19.7) | 23 | 351 |
| gorge · random (manual) | 103 (102–103) | 9.74 (9.70–9.78) | 232 (232–233) | 8.50 (8.48–8.54) | 42 | 349 |
| mtg-kernel · random | 18.6 (18.4–19.4) | 53.8 (51.6–54.5) | 3,075 (3,011–3,113) | 57.2 (56.3–57.7) | 17 | 15.7 |
| XMage · random | 2.50 (2.40–2.58) | 400 (387–416) | 13,063 (12,774–13,145) | 495 (487–496) | 31 | 1,228 |
| dirty-dirac · hbot | 1,457 (1,439–1,462) | 0.686 (0.684–0.695) | 38.1 (38.1–38.6) | 6.76 (6.75–6.84) | 18 | 490 |
| gorge · bot (auto-pay) | 103 (102–103) | 9.70 (9.68–9.83) | 540 (538–544) | 21.5 (21.4–21.7) | 18 | 346 |
| gorge · bot (manual) | 134 (132–136) | 7.45 (7.37–7.58) | 412 (410–417) | 14.7 (14.7–14.9) | 18 | 355 |
| XMage · simple AI | 19.6 (19.5–19.7) | 50.9 (50.7–51.3) | 747 (743–753) | 47.4 (47.1–47.8) | 68 | 1,218 |
| XMage · mad AI | 1.22 (1.18–1.25) | 822 (801–850) | 51,169 (49,069–51,492) | 2,349 (2,292–2,391) | 16 | 1,286 |
| Forge · Forge AI | 0.807 (0.723–0.966) | 1,239 (1,035–1,383) | 67,548 (58,991–71,387) | n/a | 18 | 868 |
Burn mirror¶
ad3777023, mtg-kernel 058d075e3, one pinned core per run (24/26/28); scripts/xengine-bench.sh. Filled bars: random legal action; outlined bars: the engine's own AI.| Engine · policy | games / core-s | ms / game | µs / turn | µs / decision | turns / game | peak RSS (MB) |
|---|---|---|---|---|---|---|
| gorge · random (auto-pay) | 143 (142–143) | 6.98 (6.97–7.06) | 267 (266–267) | 12.2 (12.1–12.2) | 26 | 354 |
| gorge · random (manual) | 118 (117–118) | 8.48 (8.45–8.53) | 240 (238–240) | 10.9 (10.9–10.9) | 36 | 353 |
| mtg-kernel · random | 174 (173–174) | 5.75 (5.74–5.78) | 367 (365–368) | 8.15 (8.11–8.15) | 16 | 10.9 |
| XMage · random | 3.14 (2.91–3.18) | 318 (315–344) | 11,188 (10,856–11,308) | 527 (508–534) | 29 | 1,262 |
| gorge · bot (auto-pay) | 111 (110–112) | 9.00 (8.96–9.05) | 437 (434–439) | 18.7 (18.6–18.7) | 21 | 347 |
| gorge · bot (manual) | 159 (158–159) | 6.30 (6.27–6.31) | 308 (306–309) | 12.1 (12.1–12.2) | 20 | 352 |
| XMage · mad AI | 0.259 (0.179–0.282) | 3,864 (3,547–5,597) | 272,821 (226,484–345,899) | 13,226 (11,010–17,280) | 16 | 1,336 |
| Forge · Forge AI | 1.58 (1.45–1.64) | 633 (609–688) | 35,533 (33,783–36,489) | n/a | 18 | 892 |
On FDN pair A, gorge's random players spend 233-471 µs a turn against mtg-kernel's 3,075 µs; on its home Burn mirror mtg-kernel is close to gorge (367 µs a turn against 240-267 µs), and its games are shorter, so it plays 174 games per core-second against gorge's 118-143. dirty-dirac remains the fastest engine on pair A by an order of magnitude.
Search bots: cost per simulation¶
A search bot clones and steps the game many times per decision, so its cost is the cost of one simulation times the simulation budget. These rows put an MCTS search on both seats (dirty-dirac) or on searched priority roots (gorge) of FDN pair A at 25, 100 and 1,000 simulations per searched decision, on one pinned core, three reps; Methodology says exactly what runs and how each is counted. They measure speed only.
ad3777023, one pinned core per run (24/26/28), load average 17-32 (core contention at most 10%). dirty-dirac counts whole games' CPU per simulation, gorge only the search's CPU.| Search bot | sims per decision | µs per simulation | simulations per core-second | ms per searched decision | ms per game | games or roots per run |
|---|---|---|---|---|---|---|
| dirty-dirac MCTS, heuristic leaf | 25 | 57.1 (56.9–58.1) | 17,499 (17,222–17,560) | 1.43 (1.42–1.45) | 108 (105–113) | 278-286-266 |
| dirty-dirac MCTS, heuristic leaf | 100 | 74.3 (73.9–79.6) | 13,455 (12,560–13,527) | 7.43 (7.39–7.96) | 546 (535–623) | 49-55-57 |
| dirty-dirac MCTS, heuristic leaf | 1000 | 108 (89.9–109) | 9,277 (9,201–11,119) | 108 (89.9–109) | 8,219 (7,263–9,428) | 4-4-4 |
| dirty-dirac MCTS, network leaf | 25 | 99.3 (96.9–103) | 10,075 (9,737–10,323) | 2.48 (2.42–2.57) | 171 (168–174) | 165-157-161 |
| dirty-dirac MCTS, network leaf | 100 | 90.0 (89.4–95.2) | 11,107 (10,508–11,185) | 9.00 (8.94–9.52) | 617 (594–632) | 51-48-49 |
| dirty-dirac MCTS, network leaf | 1000 | 177 (163–189) | 5,654 (5,298–6,142) | 177 (163–189) | 11,792 (7,978–12,205) | 4-3-3 |
| gorge MCTS (azmcts), heuristic leaf | 25 | 914 (908–966) | 1,094 (1,035–1,102) | 22.9 (22.7–24.1) | n/a | 1143-1131-1075 |
| gorge MCTS (azmcts), heuristic leaf | 100 | 942 (939–978) | 1,061 (1,023–1,065) | 94.2 (93.9–97.8) | n/a | 301-312-311 |
| gorge MCTS (azmcts), heuristic leaf | 1000 | 1,604 (1,281–1,687) | 623 (593–780) | 1,604 (1,281–1,687) | n/a | 26-20-20 |
Where a simulation's time goes, at 100 simulations:
| Search bot | Engine (re-deal, replay and expansion of the world) | Leaf evaluator | Search bookkeeping (select, backup) |
|---|---|---|---|
| dirty-dirac MCTS, heuristic leaf (79 µs a simulation) | 78.8 µs (99.7%): re-deal 1.7, replaying the line 51.2, expanding the node 25.1 | included in the engine column | 0.24 µs |
| dirty-dirac MCTS, network leaf (99 µs a simulation) | 82.6 µs (84%) | 15.7 µs (16%): building the input 10.7, the forward pass 5.0 | 0.29 µs |
| gorge MCTS, heuristic leaf (about 940 µs a simulation) | most of it: Engine.Submit 83% cumulative (game play inside the search), re-dealing the world about 3%, building the in-search bot's board 5.7% |
under 1% (heuristicLeafValue 0.15%) |
about 1% (azmcts flat) |
The dirty-dirac split is its runner's per-simulation timers (wall time
inside the search, so its totals differ slightly from the CPU-time table);
the gorge split is a 20-second CPU profile of enginebench -row az -sims 100.
In both engines almost all of a simulation is the rules engine replaying and
extending a line of play; the search itself is noise, and a small network
leaf adds about a quarter on dirty-dirac. The per-simulation cost grows with
the budget because deeper trees replay longer lines.
dirty-dirac's rows¶
Every dirty-dirac figure on this page (the game rows, the FDN pair A table and
the search rows) was measured on 2026-10-20 with the dirty-dirac source of
that day, which pinned gorge aa421da93. That was before the kernel redesign
of 2026-10-21 (dirty-dirac). The rows
have not been re-run since. The redesign's own A/B, the kernel's transition
benchmark on one pinned core against the pre-redesign kernel, counts about
4 % fewer instructions per transition on FDN and about 2 % fewer on BLB.
Instruction counts are not CPU time, so treat the rows above as dated
measurements of the earlier kernel, not as current figures.
Caveats¶
Not apples to apples
- Card coverage differs. XMage and Forge implement every card; gorge implements most of Forge's corpus; dirty-dirac plays only its generated card pools, two players; mtg-kernel plays a few hundred cards. The main workload is FDN because it is the pool the first four play; mtg-kernel plays only pair A of it.
- Policies differ. The random players are close but not identical (XMage answers non-priority asks with its base AI); the AIs differ in kind and strength. Each row's policy is spelled out in Methodology.
- Decisions are counted differently, so µs per decision is only comparable within an engine; µs per turn is the cross-engine normaliser.
- JVM numbers are steady state after 40 s of warm-up games; start-up (8-10 s for XMage, about 6 s for Forge to load cards) is not in the per-game numbers.
- One shared machine. Three reps of a row ran at once on three physical cores; a gorge and a dirty-dirac row run alone on one core came out within 4% of the concurrent runs.
Provenance¶
| Date | 2026-10-20: FDN pairs A and B 17:45-18:07 UTC; search rows 18:35-18:41 UTC; mtg-kernel and the Burn mirror 18:50-19:00 UTC |
| Machine | AMD Ryzen 9 9950X (16 cores, 32 threads), 120 GB, Linux 6.17, performance governor |
| Cores | one per run: 24, 26, 28 (three reps at once); GOMAXPROCS=1 for Go; JVMs pinned the same way (serial GC, -Xmx2g) |
| Load | 1-minute load average 4.4-15 for the FDN A/B tables (per-row ranges in the per-pair table), 6-36 for the Burn rows, 17-32 for the search rows; no published rep had more than 10% of its core taken by other work except one XMage mad pair-A rep (28%), whose result sits inside the other two reps' spread |
| gorge | ad3777023 (develop), Go 1.26.3, cmd/enginebench |
| dirty-dirac | private kernel, the source of 2026-10-20 (before the redesign), which pinned gorge aa421da93 |
| XMage | magefree/mage 6b602a1c85 (1.4.61), Temurin JDK 17.0.20.1, driver XEngineBench (gorge-compliance) |
| Forge | gorge's Forge fork at da1c712c31 (forge-core/game/ai 2.0.16-SNAPSHOT), JDK 17.0.20.1, driver forge.oracle.GameReplay |
| mtg-kernel | jackmaiorino/mtg-kernel 058d075e3 (2026-10-20), Rust 1.94.1, release build of examples/bench_kernel with gorge's --xengine-json mode; FDN pair A from the limited-fdn-fixtures build, Burn from the default build |
| Command | SECS=30 scripts/xengine-bench.sh gorge dirty-dirac, then SECS=60 WARM=40 scripts/xengine-bench.sh xmage forge, with CORES="24 26 28"; the Burn rows the same with PAIRS=burn; mtg-kernel with MTGKERNEL_BENCH=… MK_PAIRS=A (FDN build) and MK_PAIRS=burn (default build); search rows as in Methodology |
| Measured time | 30 CPU-s per gorge and dirty-dirac run; 40 s warm-up + 60 CPU-s per XMage and Forge run |
| Errors | no stalled, unfinished or failed game in any published row. XMage's base AI on the Burn mirror ended every game at the 100-turn cap without a winner (it never casts its burn spells), so that row is left out |
Engines that are not on this page were not installed on the measuring machine; Methodology says what adding one takes.