Skip to content

compiler: make TARGET_ATTRIBUTE and LIKELY/UNLIKELY capability checks consistent under clang-cl - #4729

Open
matevz-kovacic wants to merge 2 commits into
facebook:devfrom
matevz-kovacic:clang-cl-target-attribute
Open

compiler: make TARGET_ATTRIBUTE and LIKELY/UNLIKELY capability checks consistent under clang-cl#4729
matevz-kovacic wants to merge 2 commits into
facebook:devfrom
matevz-kovacic:clang-cl-target-attribute

Conversation

@matevz-kovacic

Copy link
Copy Markdown

Summary

TARGET_ATTRIBUTE and LIKELY/UNLIKELY in lib/common/compiler.h are
gated on __GNUC__, which clang-cl does not define — although clang-cl
supports both features. A clang-cl build therefore emits none of the
BMI1/BMI2 instructions the dynamic dispatch exists to select, and discards
every branch hint in the library.

This replaces the __GNUC__ test with a direct
capability test (__has_attribute / __has_builtin), keeping __GNUC__
as a fallback so nothing changes for compilers without __has_*.

Missing under clang-cl: 683 instructions — shrx 377, shlx 240,
bzhi 31, lzcnt 27, andn 8. The bzhi are the ones #2689 introduced to
replace BIT_mask lookups. Why: DYNAMIC_BMI2 is enabled under clang-cl,
so dispatch is active, but BMI2_TARGET_ATTRIBUTE expands to nothing — the
_bmi2 entry points it calls were compiled without BMI2. Nothing is
miscompiled and nothing faults; the dispatch is paid for and returns
nothing.

Worth ~11% decompression speed using clang-cl on the machine measured below.

Commit 1: TARGET_ATTRIBUTE
Commit 2: LIKELY / UNLIKELY

Are other macros affected the same way?

I audited every __GNUC__ gate in lib/. These two are the only ones with
code-generation consequences under clang-cl. The rest fall into three
groups:

  • Saved by an earlier _MSC_VER branch, which clang-cl takes:
    FORCE_INLINE_ATTR (__forceinline), FORCE_NOINLINE
    (__declspec(noinline)), PREFETCH_L1/L2 (_mm_prefetch), and the
    count-leading/trailing-zeros paths in bits.h (_BitScanForward /
    _BitScanReverse).
  • Cosmetic: UNUSED_ATTR, MEM_STATIC, ERR_STATIC — warning
    suppression and inline spelling, no codegen effect.
  • Falls through, but costs nothing: MEM_FORCE_MEMORY_ACCESS is left
    undefined under clang-cl, selecting the memcpy path rather than the
    packed-struct one. Building with -DMEM_FORCE_MEMORY_ACCESS=1 produces
    byte-identical objects (30/30), because clang lowers the constant-size
    memcpy to the same load. Nothing to gain.

ZSTD_ASM_SUPPORTED in portability_macros.h is also __GNUC__-gated and
does evaluate to 0 for clang-cl — but that is the correct answer, since the
.S files are GNU-syntax and clang-cl will not assemble them.

Generated code

Instructions requiring the BMI2 target attribute, stock Release build of
the static library:

toolchain __GNUC__ before after
mingw gcc 16.1.0 yes 705 705
clang 22.1.8 --target=x86_64-w64-mingw32 yes 683 683
clang-cl 22.1.8 no 0 683
MSVC (VS 2026, cl x64) no 0 0

The same clang binary already emits exactly 683 in GNU driver mode; this
patch brings it to the same 683 in MSVC driver mode.
Same compiler, same
source, same flags — only the driver differed. MSVC's 0 is correct and
expected: it has no __target__ equivalent.

These counts exclude tzcnt, which is not gated by the target attribute —
its encoding is backward-compatible with bsf, so compilers emit it freely
(clang-cl emits 565 either way, gcc 1622). Counting it would obscure the
effect rather than show it.

Effect on other compilers: none

Both macros already expand wherever __GNUC__ is defined, so the patched
header should produce identical machine code there. Verified by diffing
per-object disassembly of baseline vs patched builds: gcc 16.1 30/30
objects byte-identical, clang --target=x86_64-w64-mingw32 32/32,
MSVC cl 30/30. Only clang-cl changes.

Correctness

The _bmi2 entry points now genuinely require BMI2, so this rests on the
existing runtime dispatch. Every one of the 683 instructions lands in a
function annotated BMI2_TARGET_ATTRIBUTE or HUF_FAST_BMI2_ATTRS — none
leaked into a shared helper — so they are reached only through that
dispatch.

Checked by running it: a build with ZSTD_cpuSupportsBmi2() forced to 0
and cctx->bmi2 = 0, simulating a CPU without BMI2, passes round-trip and
cross-decode against a stock decoder while still containing all 683
instructions — present but never executed.

This is upstream's existing mechanism rather than new risk: a gcc build of
the unmodified baseline already places 705 such instructions inside those
same dispatched functions.

Benchmarks

One machine, one OS, one compiler — not a cross-platform claim.

i5-13400, Windows 11, clang-cl 22.1.8, baseline v1.5.7
(f8745da6ff1ad1e7bab384bd1f9d742439278e99), identical flags both arms.
zstd -b -d -i5 --single-thread, pinned to a single P-core at high
priority, arms interleaved within every repeat, median of 11 repeats,
aggregated over total corpus time. Levels 1, 3, 5, 8, 12, 16, 19. Corpora:
Silesia, plus a held-out set (enwik8, a source tarball, a large JSON file,
a tarball of native x86-64 binaries).

Decompression speed:

Silesia Silesia (repeat run) held-out
commit 1 only +4.97% +4.87% +4.50%
commits 1+2 +11.20% +10.96% +11.49%

Every level improved, in all three runs. For commits 1+2, at every level
the slowest of the 11 patched samples was still faster than the fastest of
the 11 baseline samples — the two sets never overlap. Re-running the
Silesia measurement in a separate timing window reproduced it to within
0.24 percentage points.

Compression ratio: unchanged, and structurally so — the patch changes
only which instructions compute the same values, not any encoding
decision. Confirmed anyway: every corpus file recompressed with both
binaries at every level, 112 / 112 byte-identical by SHA-256.

Compression speed: measured, not assumed. 217 of the 683 instructions
land on the compression side (huf_compress +182,
zstd_compress_sequences +35), and a target-attributed function can no
longer be inlined into a non-attributed caller — so a compression effect in
either direction is possible in principle. Measured: +0.40% overall. I
claim no improvement; run-to-run variation at the high levels reaches ~20%
on this machine, so the honest statement is only that no regression was
detectable. I did not separately isolate the inlining cost.

Notes

Commit 2 is independent and can be dropped if you would rather consider
branch-hint changes separately. It adds no annotations — those are already
in the tree (#2689 added several to ZSTD_decodeSequence) and this only
stops one driver mode from discarding them. Its contribution is measured
separately above because compiler.h asks that these macros earn their
place.

Not tested: 32-bit clang-cl; a genuinely pre-BMI2 CPU (that path was
exercised by forcing dispatch off, not on such hardware); the cost of the
attribute's inlining restriction in isolation.

Tested:

  • tests/fuzzer and tests/zstreamtest (-T90s -s1) built with clang-cl
    from both the baseline and the patched tree — both exit 0 on both arms.
    These are the ones that exercise the changed code.
  • Round-trip and cross-decode both ways against a stock build, across
    Silesia, the held-out set, and a generated adversarial set.
  • tests/playTests.sh passes on Linux (WSL/Ubuntu, gcc 13.3): exit 0, 55
    sections, with only the usual conditional skip of the large-data tests.
    Worth noting what that does and does not show — on gcc this patch is a
    byte-identical no-op, so it confirms the header change breaks nothing in a
    POSIX build rather than exercising the new code generation.

portability_macros.h enables DYNAMIC_BMI2 for clang-cl by testing
__clang__ && __has_attribute(__target__). compiler.h then gates
TARGET_ATTRIBUTE on __GNUC__, which clang-cl does not define, so
BMI2_TARGET_ATTRIBUTE expands to nothing and the dispatched _bmi2
entry points are compiled without BMI2.

Test the attribute directly with __has_attribute, keeping the __GNUC__
test as a fallback for compilers without __has_attribute. No change on
any compiler that defines __GNUC__.
Same __GNUC__ gate as TARGET_ATTRIBUTE. clang-cl supports
__builtin_expect, so test for it with __has_builtin and keep __GNUC__
as a fallback. Without this, every branch hint in the library is
discarded under clang-cl.
@meta-cla meta-cla Bot added the CLA Signed label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant