Skip to content

indexcodec: Optimize decoding for GCC/Clang on x64#1064

Merged
zeux merged 4 commits into
masterfrom
idxopt
Jul 11, 2026
Merged

indexcodec: Optimize decoding for GCC/Clang on x64#1064
zeux merged 4 commits into
masterfrom
idxopt

Conversation

@zeux

@zeux zeux commented Jul 10, 2026

Copy link
Copy Markdown
Owner

This change implements several optimizations to existing index decoder,
mainly targeting Clang/GCC on x86-64.

fec == 0 branch is difficult to predict; the fast path (edge reuse)
decoding is intentionally written in a way that promotes branchless code
generation, as reducing the mispredictions saves more time than the extra
executed instructions take.

Unfortunately, both clang and gcc compile it as a branch, for two different
(bad) reasons. clang uses x86-cmov-conversion pass that replaces select
with a branch because one of the sources is a memory load. This could be fixed
with __builtin_unpredictable; however, GCC doesn't support it. GCC takes
the code that loads cf and moves it into the ternary, which then makes the
ternary look like a branch where one side is expensive and one side is cheap.
Attempts to use __builtin_expect_with_probability do not help.

What does help is using an asm barrier that uses cf; this fixes both problems
on both compilers. Additionally, GCC has problems with edgefifo use on some
versions: it can fuse a/b loads into a single 64-bit load but keep edgefifo
stores as 32-bit, which breaks store-to-load forwarding and results in significant
performance loss too.

Once these issues are fixed, clang suffers from register pressure because of the
indexed load. So this change also reworks the loop structure so that we never
need the triangle index. Finally, the data bounds checks are only needed on
the paths that read from data, and these paths are fairly cold so we can move
these there too.

Altogether this makes index decoder ~10-20% faster depending on the compiler
and data set. MSVC performance is not affected to a significant degree; it already
correctly emitted cmov but has some other weaknesses that are more difficult
to fix, so Clang produces the fastest code. Clang can get up to 25% faster on
large meshes with few seams because the fast path is used for the majority of
the triangles.

This contribution is sponsored by Valve.

zeux added 3 commits July 9, 2026 11:05
In addition to vK_ prefixed files we now support iK_ prefixed files that
contain encoded index data. The file names encode the index count; we do
not technically know the max index. To exercise both 16-bit and 32-bit
decoding paths, we guess the maximum index from the index count.
fec == 0 branch is difficult to predict; the fast path (edge reuse)
decoding is intentionally written in a way that promotes branchless code
generation, as reducing the mispredictions saves more time than the extra
executed instructions take.

Unfortunately, Clang's x64-cmov-conversion pass assumes that cmov
dependent on a memory read (vertexfifo here) is unprofitable and
converts it back to a branch, regressing the performance. Because the
issue is Clang specific, we can use a Clang specific attribute to flag
that the branch is unpredictable.

This fixes codegen to use cmov on Clang when targeting x86_64 and makes
decoding ~10% faster. This was not a problem for other architectures
like aarch64.
We rework the code to avoid materializing the triangle index; we are
short on registers and this code is sensitive to the choice of induction
variables. This also reduces the address generation complexity for the
triangle stores.

Also take this opportunity to move the data bounds check inside the two
slow paths it's necessary in; two fast paths don't read data and don't
need it. This makes data_safe_end less hot which frees up a register
too. And replace a branchless +/-1 with simpler code.

Altogether this results in ~5% faster index decoding on clang/gcc.
@zeux
zeux force-pushed the idxopt branch 2 times, most recently from 1d9c6dc to 614184c Compare July 10, 2026 19:56
gcc has the same problem as clang, but it can't be solved using an
attribute: gcc doesn't support __builtin_unpredictable and
__builtin_expect_with_probability does not affect the codegen here.

Additionally, some versions of gcc (gcc-16+, possibly earlier versions)
can fuse two reads from edgefifo[] for a/b into a single 64-bit load,
but keep separate stores into edgefifo[]. This breaks store to load
forwarding, as it usually requires that a load is covered by a single
store, which results in further significant performance loss.

Both of these can be fixed with an asm barrier: cf is forced to be
materialized in a register which prevents folding it into the ternary,
at which point the select is simple and emits cmov; the +r restriction
on a also blocks fusing the loads into a single load.

While clang could technically keep using the attribute workaround, the
same workaround appears to work fine for it, so for simplicity we use
the same asm barrier for both compilers for now.

This makes index decoding ~10-12% faster for gcc14/15 and ~19% faster
for gcc16.
@zeux
zeux merged commit 441ff30 into master Jul 11, 2026
13 checks passed
@zeux
zeux deleted the idxopt branch July 11, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant