The compiler is measured on twenty-two programs, each written four times: in the
xc language, in Objective-C with ARC, in C++ and in Swift, doing the same work
with the same algorithm and the same data. All are built with optimisation
(-O3 for xc, Objective-C and C++, -O for Swift), every version prints a
checksum, and a run only counts if all four checksums agree. Every program, in
all four languages, is on the benchmark sources
page; each benchmark’s name below links to its own. On x86-64 the C++ source
is built twice, with clang and with GCC (g++ -O3), on both targets, so each
carries a column for each C++ compiler.
Each figure is measured with the released 1.0 xcc on two machines:
arm64: an Apple MacBook Pro with an M3 Max (12 performance and 4
efficiency cores, a 40-core GPU, no SME matrix unit), macOS; Apple clang 17.0.0, GCC 16.2.0 and Swift 6.1.2;
x86-64: an AMD Ryzen 9 9955HX (16 cores, 32 threads), Linux; GCC 15.2.0, clang 21.1.8 and Swift 6.1.3; its
CPU frequency governor is performance (amd-pstate-epp) for the run.
Every compiler is given the machine’s full instruction set: -march=native
(x86-64) or -mcpu=native (arm64) for clang and GCC, -target-cpu native for
Swift. xc needs no flag for that: with its default -msimd=auto, xcc builds
each vectorised function for SSE2, AVX2 and AVX-512 and the program picks one
when it starts. Nothing else is set: no link-time optimisation, no
profile-guided build, no -ffast-math, on any side.
Every program runs on one thread: xc’s par blocks, and the loops xcc lifts
into them, are held to one CPU thread for this suite, and the other
languages’ builds use no threading. SIMD is used on every side.
Times are seconds for the timed region, the best of five runs, each run waiting
until the machine is otherwise idle.
How much faster xc’s code runs than each language’s, as the geometric mean
over eighteen of the twenty-two benchmarks:
xc compared with
arm64 (Apple M3 Max)
x86-64 (AMD Ryzen 9 9955HX)
Objective-C
1.42× faster
1.34× faster
C++ (clang)
1.13× faster
1.10× faster
C++ (GCC)
1.29× faster
1.30× faster
Swift
1.47× faster
1.56× faster
On both targets xc is ahead of all of them.
Four benchmarks are kept out of those means. xcc recognises the loop nest of
matrix_mul_f32 and replaces it with a matrix kernel of its own, which makes xc
tens to hundreds of times faster at that one operation; counted in, it would
dominate means that stand for ordinary code, and most programs do not multiply
matrices. Its figures are under Matrix multiplies.
method_call makes a virtual call in a loop, which xcc turns into a class test and a direct call it can inline, so xc runs it 11.9× faster on arm64 and 57× faster on x86-64 than C++ (clang). The result is real, and it is in every table and chart below, but one ratio that large would move a mean of ordinary code by itself.
int_muldiv_hoist and array_sum_hoist measure whether a compiler notices that
an outer loop repeats the same work on every pass. xcc does, and runs both in
microseconds, so their ratios say nothing about ordinary code; the tables show
them as “repetition removed”.
Each row is a benchmark, each dot one language: how much faster or slower xc
is than that language. Dots left of the centre line are benchmarks xc wins. Choose a
release above a chart to see how it stood then; the rows stay in 1.0’s
order. On arm64 every release’s xc was timed again on the Apple M3 Max with today’s programs, and is set against the other languages as measured there for 1.0. On x86-64 releases before 0.65 were measured against Objective-C alone, apart from matrix_mul_f32, which was added later and measured against all three.
Release:
arm64: xc against each language, per benchmark (0.62)
arm64: xc against each language, per benchmark (0.63)
arm64: xc against each language, per benchmark (0.64)
arm64: xc against each language, per benchmark (0.65)
arm64: xc against each language, per benchmark (0.66)
arm64: xc against each language, per benchmark (0.7)
arm64: xc against each language, per benchmark (0.71)
arm64: xc against each language, per benchmark (0.72)
arm64: xc against each language, per benchmark (0.73)
arm64: xc against each language, per benchmark (0.74)
arm64: xc against each language, per benchmark (0.75)
arm64: xc against each language, per benchmark (0.8)
arm64: xc against each language, per benchmark (0.85)
arm64: xc against each language, per benchmark (0.86)
arm64: xc against each language, per benchmark (0.87)
arm64: xc against each language, per benchmark (0.87.3)
arm64: xc against each language, per benchmark (0.88)
arm64: xc against each language, per benchmark (0.89)
arm64: xc against each language, per benchmark (0.9)
arm64: xc against each language, per benchmark (0.9.1)
arm64: xc against each language, per benchmark (0.9.2)
arm64: xc against each language, per benchmark (0.9.3)
arm64: xc against each language, per benchmark (0.9.4)
arm64: xc against each language, per benchmark (0.9.5)
arm64: xc against each language, per benchmark (0.9.6)
arm64: xc against each language, per benchmark (0.9.7)
arm64: xc against each language, per benchmark (1.0)
Release:
x86-64: xc against each language, per benchmark (0.62)
x86-64: xc against each language, per benchmark (0.63)
x86-64: xc against each language, per benchmark (0.64)
x86-64: xc against each language, per benchmark (0.65)
x86-64: xc against each language, per benchmark (0.66)
x86-64: xc against each language, per benchmark (0.7)
x86-64: xc against each language, per benchmark (0.71)
x86-64: xc against each language, per benchmark (0.72)
x86-64: xc against each language, per benchmark (0.73)
x86-64: xc against each language, per benchmark (0.74)
x86-64: xc against each language, per benchmark (0.75)
x86-64: xc against each language, per benchmark (0.8)
x86-64: xc against each language, per benchmark (0.85)
x86-64: xc against each language, per benchmark (0.86)
x86-64: xc against each language, per benchmark (0.87)
x86-64: xc against each language, per benchmark (0.87.3)
x86-64: xc against each language, per benchmark (0.88)
x86-64: xc against each language, per benchmark (0.89)
x86-64: xc against each language, per benchmark (0.9)
x86-64: xc against each language, per benchmark (0.9.1)
x86-64: xc against each language, per benchmark (0.9.2)
x86-64: xc against each language, per benchmark (0.9.3)
x86-64: xc against each language, per benchmark (0.9.4)
x86-64: xc against each language, per benchmark (0.9.5)
x86-64: xc against each language, per benchmark (0.9.6)
x86-64: xc against each language, per benchmark (0.9.7)
x86-64: xc against each language, per benchmark (1.0)
The same programs on a Raspberry Pi 5 Model B Rev 1.0 (Cortex-A76) under Raspberry Pi OS, from 0.89: xc cross-built with -A arm64-linux, the C++ built on the Pi by clang++ 19.1 and g++ 14.2, all at -O3. Seconds; the best of five runs.
Geometric mean of xc’s times against each: 1.10× faster than clang++, 1.25× faster than g++.
The fastest results are where the runtime does the work: string_scan,
method_call and struct_copy are byte scanning, dynamic dispatch and
aggregate copies. The slowest show where xcc’s code generation has most to
gain:
Vectorisation. On arm64, int_muldiv and float_math are vectorised
by both compilers, and clang’s loops are tighter; matrix_mul has matched
clang there since 0.73. On x86-64 matrix_mul is still behind, because
clang keeps the second matrix’s rows in registers across the outer loop.
Reference counting on x86-64.arc_array, where C++ reads elements
without retaining them.
x86-64 loops and calls.sieve and sort_small, where clang’s loops
are still tighter (in 0.74 sort_small was 2.6× behind; 0.75 rotates its
inner loop), and call_depth, where clang’s call sequence is shorter.
A dense matrix multiply written as three plain loops, C[i][j] the sum over
k of A[i][k] · B[k][j] in float or double, runs as a kernel the
compiler writes itself (from 0.71). The results are the loops’, to the last
bit, and -fno-matmul turns it off. The kernel depends on the machine:
Apple M4 and later (SME): the multiply runs on the matrix unit. From
0.72 it works on a 2×2 block of the unit’s tiles at once, and under
:goal(speed),
which matrix_mul_f32 uses, it leaves out a NaN check that only affects the
bits of a NaN (the inputs have none).
Apple M1 to M3 (no SME): the program finds no matrix unit when it
starts and runs the loops, vectorised. The arm64 figures on this page, for
every release, come from an M3 Max, so there matrix_mul_f32 is the loop;
the table below adds an M4 Max, where it is the matrix unit.
x86-64: the widest vector unit the processor has (SSE2, AVX2 or
AVX-512), chosen when the program starts; the Ryzen 9 9955HX picks AVX-512.
matrix_mul_f32 measures it: a 128×128 float multiply, 10,000 times.
(matrix_mul multiplies u32 values, which the matrix unit does not take, so
it stays a vectorised loop.)
par blocks, which run on every CPU thread, on GPUs and across machines, have
pages of their own: Performance: GPU (including
plain loops made parallel by --par-style=auto) and
Performance: networked. Plain C built by xcc,
against GCC and clang on one thread and with OpenMP on every thread, is on
Performance: C.
How fast xc’s code is in each release, relative to 0.62: above 1× is faster.
The thin lines are single benchmarks (labelled where they moved by more than
twelve percent), the thick line the geometric mean (over the benchmarks the means cover, as in the summary). Releases shown: 0.62, 0.63, 0.64, 0.65, 0.66, 0.7, 0.71, 0.72, 0.73, 0.74, 0.75, 0.8, 0.85, 0.86, 0.87, 0.87.3, 0.88, 0.89, 0.9, 0.9.1, 0.9.2, 0.9.3, 0.9.4, 0.9.5, 0.9.6, 0.9.7, 1.0. arc_alloc, method_call changed in
0.64’s benchmark set and are left out of this history.
arm64: xc's speed relative to 0.62x86-64: xc's speed relative to 0.62
The compiler that ships. The xc numbers come from the xcc in the download.
The same work in every language. Every version keeps its data where the xc
version keeps it (local arrays, not static ones, which clang optimises
differently) and leaves nothing a compiler can remove: an object that could be
put on the stack outlives its iteration, a call whose target could be resolved
at compile time takes its class at run time, and results are folded in so no
loop has a closed form. Where the original’s point is reference-counted
objects, the C++ version uses std::shared_ptr and the Swift version a class,
so they pay for reference counting too.
Different runtimes on the two targets. The Objective-C column is Apple’s
Foundation on arm64 and GNUstep with libobjc2 on x86-64, and Swift is 6.2 on the
Mac and 6.1 on Linux. These are different implementations, so a language’s
times compare within a target and not across one. Swift on Linux is markedly
slower on poly_dispatch and array_map than on the Mac; the runs were
repeated on an idle machine and reproduce.
Timed regions of about one second. Each benchmark times its own inner loop
rather than the process, so start-up and data set-up are excluded.
Alignment noise on x86-64. xcc aligns every loop head on x86-64 to a 32-byte
boundary and the start of .text to 64 bytes, so an unrelated change elsewhere
cannot move a loop across a fetch boundary; see
Optimisation.
The sources are in benchmark/src, one .xc, .m, .cpp and .swift per
program, and the runner builds and times them all:
python3 benchmark/run.py --opt O3 --repeats 5
python3 benchmark/page.py
The x86-64 legs cross-build xc here and build the other languages on the
configured Linux host; without one the runner measures this machine only.
--langs measures some of the languages and adds them to the results already
there. Results land in benchmark/<version>/results.json, and page.py turns
them into this page.