Skip to content

Benchmarks

ffredyk edited this page Jul 24, 2026 · 1 revision

Performance Benchmarks

SQ# versus Arma 3 SQF engine performance analysis. Single-fiber benchmarks, parallelism scaling, and why raw speed is not the primary goal.

Executive Summary

SQ# executes SQF-syntax scripts on .NET runtime. On single-fiber compute-bound workloads, SQ# averages 2.4× slower than Arma 3's native SQF engine (range: 0.81× to 10.2×). SQ# is faster than Arma on 2 of 21 benchmarks: splitString/joinString and forEach-math.

This is expected, acknowledged, and not a priority to fix. Performance parity with Arma 3's single-threaded engine is neither the goal nor within practical reach given fundamental architectural constraints.

However: single-fiber benchmarks measure SQ#'s worst case. SQ#'s primary advantage is parallelism — the ability to run multiple schedulers on separate threads simultaneously. Arma 3 cannot do this at all.

The Parallelism Factor

Every single-fiber benchmark runs one script on one scheduler — Arma 3's exact execution model. This is SQ#'s worst case and Arma's best case. Real server workloads don't look like this.

The Arma 3 Bottleneck

Arma 3 executes ALL scripts on one simulation thread. There is no parallelism. At 30 FPS, scripts get ~3–6ms per frame after rendering, physics, and networking.

Consider a server with 200 AI units, each running a 1ms behavior script per tick:

200 scripts × 1ms = 200ms required
3ms per frame = 67 frames to process all scripts
At 30 FPS = 2.2 seconds of latency per AI decision cycle

Scripts queue up serially. The server's AI becomes sluggish, reaction times degrade. This is Arma 3's fundamental scalability limit.

SQ# Parallel Schedulers

SQ# provides named schedulers running on separate threads:

// Distribute work across scheduler threads:
[unitData] spawnOn ["AI_1", { aiBehavior(_this); }];
[unitData] spawnOn ["AI_2", { aiBehavior(_this); }];
[unitData] spawnOn ["AI_3", { aiBehavior(_this); }];
[unitData] spawnOn ["AI_4", { aiBehavior(_this); }];

Same 200-unit workload, 4 schedulers:

200 scripts ÷ 4 schedulers = 50 scripts per scheduler
50 scripts × 1ms = 50ms per scheduler
3ms budget × 17 ticks = ~0.56 seconds wall-clock

200 scripts complete in 0.56s instead of 2.2s — a 4× improvement from parallelism alone. Add more schedulers (up to available cores), latency drops further.

Real-World Comparison

Scenario Arma 3 SQ# (4 schedulers) Winner
1 script, heavy math 169 ms 464 ms Arma
200 scripts, light AI 2,200 ms (serial) 560 ms SQ#
500 scripts, mixed 5,500+ ms (serial) ~700 ms SQ#

At ~50 concurrent scripts of moderate complexity, SQ#'s parallelism advantage exceeds Arma's per-fiber speed advantage. For multiplayer servers, headless clients, and dedicated AI hosts — parallelism is the dominant factor.

Measured Parallelism Results

From bench-parallel.sqf: 8 fibers × 40,000 trig iterations (sin × cos + sqrt) = 320,000 math calls total.

Engine Mode Time vs SQ# Seq
SQ# sequential (1 thread) 1,556 ms 1.0×
SQ# parallel (4 threads) 418 ms 3.7× faster
Arma 3 loading screen (bare engine) 475 ms 3.3× faster
Arma 3 in-game (physics+render+AI) 2,040 ms 1.3× slower

Fair Comparison Context

SQ# currently runs with zero game overhead — no rendering, physics, AI, networking. Equivalent to Arma's loading screen. The loading screen result (475 ms) is Arma's best-case SQF speed.

In-game, Arma scripts compete with everything for the single simulation thread. The 2,040 ms result is real-world Arma performance. SQ# sidesteps this — scripts get dedicated scheduler threads.

Bottom line: SQ# on 4 threads (418 ms) matches Arma's best-case single-thread speed (475 ms).

Single-Fiber Benchmark Results

All tests: Windows 11, .NET 10, Release build. Arma 3: identical .sqf scripts in-mission.

Timer resolution note: Arma 3's diag_tickTime resolution is ~1ms (quantized to ~0.976ms). SQ# achieves sub-microsecond precision. Sub-10ms comparisons are not meaningful.

Math Throughput

Test Scale SQ# Arma 3 Ratio
Trig loop (sin+cos+tan+sqrt+log+pow) 50,000 ops 464 ms 169 ms 2.7×

Each iteration: 5 trig calls + 1 sqrt + 1 log + 2 pow + 1 add. SQ#: ~108,000 math calls/sec. Arma: ~296,000 math calls/sec.

Array Operations

Test Scale SQ# Arma 3 Ratio
pushBack 2,000 elements 3.7 ms 0.98 ms 3.8×
select (indexed read) 2,000 elements 7.9 ms 2.93 ms 2.7×
forEach 2,000 elements 3.9 ms 1.95 ms 2.0×
sort 2,000 elements 5.6 ms 1.95 ms 2.9×
find (last element) 2,000 elements 4.1 ms 1.95 ms 2.1×

String Operations

Test Scale SQ# Arma 3 Ratio
Concatenation (+) 1,000 ops 2.3 ms 1.95 ms 1.2×
format 1,000 ops 4.6 ms 2.93 ms 1.6×
splitString + joinString 1,000 ops 12.2 ms 15.1 ms 0.81× 🏆 SQ# faster!
find (substring) 1,000 ops 2.1 ms 0.98 ms 2.1×
toUpper + toLower 1,000 ops 2.7 ms 0.98 ms 2.7×

SQ# wins at splitString/joinString — .NET's string handling outperforms Arma's custom implementation.

HashMap Operations

Test Scale SQ# Arma 3 Ratio
set 1,000 keys 3.1 ms 1.95 ms 1.6×
get 1,000 keys 6.8 ms 2.93 ms 2.3×
set + get mixed 1,000 ops 5.1 ms 2.93 ms 1.7×

Loop Overhead

Test Scale SQ# Arma 3 Ratio
for empty body 30,000 iter 39.7 ms 3.91 ms 10.2×
while empty body 30,000 iter 42.2 ms 25.9 ms 1.6×
forEach empty body 30,000 iter 59.5 ms 42.0 ms 1.4×
for with math body 30,000 iter 74.0 ms 18.1 ms 4.1×
while with math body 30,000 iter 83.2 ms 42.0 ms 2.0×
forEach with math body 30,000 iter 59.2 ms 62.0 ms 0.95× 🏆 SQ# faster!

The for-loop gap (10.2×) is the largest performance difference. Arma JIT-compiles for loops to native machine code; SQ# interprets 12+ bytecode instructions per iteration. forEach-math is essentially tied — .NET's delegate invocation cost amortizes over the math body.

Summary

Average ratio across all comparable tests: SQ# is 2.4× slower.

SQ# wins in 2 of 21 tests:

  • splitString+joinString: 0.81× (12.2ms vs 15.1ms)
  • forEach-math: 0.95× (59.2ms vs 62.0ms — essentially tied)

Why SQ# Is Slower

1. Interpreter vs. JIT Compiler

Arma 3 has a JIT compiler for SQF bytecode. Hot scripts compile to native x86-64 machine code with register allocation and instruction scheduling.

SQ# is a stack-based bytecode interpreter. Every instruction goes through a C# switch dispatch, operand decoding, stack manipulation, and managed method calls. Zero native code generation.

Per-instruction overhead:

  • Array bounds check on instruction list
  • Switch table dispatch (indirect branch)
  • Operand decoding (bitwise extraction)
  • Stack pointer manipulation (bounds-checked access)
  • Method call overhead for command handlers

Arma avoids ALL of this for hot paths.

2. Managed Runtime Overhead

.NET Garbage Collection. Every SqArray, SqHashMap, string, List<SqValue> buffer, and temporary allocation goes through the GC. Unpredictable pauses. Arma 3 uses arena allocators — zero GC, deterministic deallocation.

Bounds checking. Every _stack[_sp], _locals[slot], array access passes CLR bounds verification. Arma's native code omits proven-safe bounds checks.

Delegate invocations. Every SQF command is a C# delegate — an indirect call through a function pointer. Arma resolves commands to direct native function pointers, then inlines the body — completely eliminating call overhead.

Struct copying. SqValue is 24 bytes (type tag + double + object ref). Every push/pop/store/load copies 24 bytes. In a 50,000-iteration loop with 5 ops/iter, that's ~500,000 struct copies. Arma uses NaN-boxing — 8 bytes per value in a single register.

3. Command Dispatch Path

Every command in SQ# follows:

BinaryCall opcode
  → Pop() right operand         (stack + bounds)
  → Pop() left operand          (stack + bounds)
  → ResolveCommandId(index)     (array lookup)
  → Bounds check command ID
  → Null check handler
  → Delegate invocation         (indirect call)
  → UnwrapNumber (for math)     (type check + extract)
  → Math.Sin / Math.Cos / ...   (actual work — ~20% of path)
  → new SqValue(result)         (struct construction)
  → Push(result)                (stack + bounds)

~80% interpreter overhead, ~20% actual computation. Arma inlines to a single FSIN x86 instruction.

4. String Immutability

.NET strings are immutable. Every + allocates. Every format allocates a StringBuilder + result. Every splitString allocates substring array. Every toUpper/toLower allocates.

Arma uses a string interning pool with mutable temporary buffers — no per-allocation GC pressure.

5. Loop Compilation Gap

SQ# for loop bytecode per iteration (12 instructions):

PushLocal _i → PushLocal _to → BinaryCall <= → JumpIfFalse exit
[body] → Pop → PushLocal _i → PushConst 1 → BinaryCall +
StoreLocal _i → Jump loopStart

Arma JIT emits 5 machine instructions:

.loop:  ;body (inlined)  |  inc eax  |  cmp eax,[to]  |  jle .loop

No interpreter, no stack, no dispatch.

Why Performance Is Not the Primary Goal

Project Mission (Priority Order)

  1. Portability — SQF scripts anywhere .NET runs (Windows, Linux, macOS). Arma is Windows-only.
  2. Embeddability — Host apps embed SQ# as a scripting engine with full control. Arma's engine is hard-coupled to the game.
  3. Multi-threading — Cooperative fibers, named schedulers, freeze/thaw, channels, shared atomics. Arma has no threading model.
  4. Developer experience — Precise errors (file:line:col), CLI tooling, documented bytecode. Arma errors are famously cryptic.
  5. Language modernization — Type annotations, verbatim strings, interpolation, hex literals, structured errors.

Speed Is Secondary

SQ# scripts control game logic, orchestrate AI, manage UI state, configure servers — not perform hot-path physics. Millisecond-level loop overhead differences are imperceptible for these workloads.

When high performance is needed for a specific operation, implement it in native C# and expose it as a registered command. The interpreter dispatch cost is a one-time overhead amortized over native execution.

Arma 3 Engine: 20-Year C++ Codebase

Arma 3's SQF engine represents two decades of optimization by Bohemia Interactive:

  • Written in C++ with x86-64 platform-specific optimizations
  • JIT compiler refined over 4 game releases
  • Arena allocators — zero garbage collection
  • Script operations on game objects are direct pointer manipulations
  • SQF performance IS the product — military simulation at 50+ FPS with thousands of scripted entities

SQ# is a small-team project implementing a compatible language on a managed runtime. Comparing the two is comparing a Formula 1 car to a reliable family sedan — different purposes, different constraints.

Platform Constraints (Non-Negotiable)

.NET's design prioritizes safety and productivity over raw speed:

  • No inline native code — C# cannot emit CPU instructions directly
  • No manual memory managementstackalloc/Span<T> help but can't eliminate GC
  • No unchecked array access — safety guarantees prevent unsafe optimizations
  • Delegate overhead — can't eliminate without runtime IL emission
  • Cross-platform — SIMD intrinsics differ between x64 and ARM64

Architectural decisions locked in:

  • SqValue: 24-byte tagged union (vs Arma's 8-byte NaN-boxing)
  • Stack VM with 1024-element fixed stack (vs register VM)
  • Separate compile/execute phases (enables tooling but prevents adaptive JIT)
  • Cooperative scheduling with time budgets (adds context-switch overhead)

What SQ# Does Better

Feature SQ# Arma 3
Platform support Windows, Linux, macOS Windows only
Embeddable Yes (NuGet packages) No (engine-locked)
Multi-threading Fibers + schedulers + channels Single-thread only
Error messages File(line:col): typed error Generic, often misleading
Type annotations Optional : int, : string None
String interpolation f"Hello {name}" format only
Verbatim strings @"C:\path" None
Hex literals 0xFF None
Structured errors try/catch with location data String-only errors
CLI tooling lex/parse/compile/run/repl Community tools only
Immutable sharing freeze/thaw None
Atomic variables shared with CAS None

Conclusion

SQ# is slower than Arma 3's SQF engine by 2–8× on single-fiber workloads. This is a direct consequence of running on a managed runtime with an interpreter, not a failure of implementation.

The project's value is not raw speed — it's portability, embeddability, multi-threading, modern tooling, and language improvements. For SQ#'s target workloads (scripting, configuration, game logic, server orchestration), current performance is adequate.

Closing the gap would require abandoning .NET for C++ (defeating the purpose), implementing a JIT compiler (years of work), or accepting unsafe optimizations that compromise reliability. None of these tradeoffs align with the project's goals.

For heavy computation, the host implements the operation in native C# and exposes it as a command — best of both worlds: SQF scripting ergonomics with native C# performance where it matters.

See Also

SQ# Wiki

Home

Engine Docs

Migration

Commands

Value Constructors

Arithmetic

Comparison

Logic

Array

String

Math

Random

Type & Introspection

HashMap

Code Execution

Concurrency

Scheduler

Thread Safety

Error

Output

Time

Multiplayer

Compiler

Clone this wiki locally