[C2] PEXT/PDEP intrinsics cause performance regression on AMD pre-Zen 3 CPUs
Alessandro Autiero
alautiero at gmail.com
Sun Nov 30 16:50:44 UTC 2025
Hi,
today I stumbled upon a performance issue with the Long.compress/expand and
Integer.compress/expand intrinsics on certain AMD processors. I discovered
this while working on an optimized varint decoder where I was hoping to use
Long.compress() to speed up bit extraction. Instead, I found my "optimized"
version was slower than my naive loop-based implementation. After some
digging, I believe I understand what's happening.
**Background**
The compress and expand methods (added in JDK 19 via JDK-8283893 [1]) are
intrinsified by C2 to use the BMI2 PEXT and PDEP instructions when the CPU
reports BMI2 support.
This works great on Intel Haswell+ and AMD Zen 3+, where these instructions
execute in dedicated hardware with approximately 3-cycle latency.
However, AMD processors from Excavator before Zen 3 implement PEXT/PDEP via
microcode emulation rather than native hardware.
This is confirmed by AMD's Software Optimization Guide for Family 19h
Processors [2], Section 2.10.2, which states that Zen 3 has native ALU
support for these instructions.
Wikipedia's page on x86 Bit Manipulation Instruction Sets [3] also
documents this behavior:
> AMD processors before Zen 3 that implement PDEP and PEXT do so in
> microcode, with a latency of 18 cycles rather than (Zen 3) 3 cycles. As a
> result it is often faster to use other instructions on these processors.
**Reproducer**
Here is a JMH benchmark that demonstrates the issue by comparing the
intrinsified path against the software fallback using ControlIntrinsic
flags:
```
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class PextPdepPerformanceBug {
// I'm not using constants to prevent constant folding
private long longValue;
private long longMask;
private int intValue;
private int intMask;
@Setup(Level.Iteration)
public void setup() {
var rng = ThreadLocalRandom.current();
longValue = rng.nextLong();
longMask = rng.nextLong();
intValue = rng.nextInt();
intMask = rng.nextInt();
}
// Long.compress (PEXT 64-bit)
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=-_compress_l",
"-Xcomp"
})
public long compressLongSoftware() {
return Long.compress(longValue, longMask);
}
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=+_compress_l",
"-Xcomp"
})
public long compressLongIntrinsic() {
return Long.compress(longValue, longMask);
}
// Long.expand (PDEP 64-bit)
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=-_expand_l",
"-Xcomp"
})
public long expandLongSoftware() {
return Long.expand(longValue, longMask);
}
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=+_expand_l",
"-Xcomp"
})
public long expandLongIntrinsic() {
return Long.expand(longValue, longMask);
}
// Integer.compress (PEXT 32-bit)
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=-_compress_i",
"-Xcomp"
})
public int compressIntSoftware() {
return Integer.compress(intValue, intMask);
}
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=+_compress_i",
"-Xcomp"
})
public int compressIntIntrinsic() {
return Integer.compress(intValue, intMask);
}
// Integer.expand (PDEP 32-bit)
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=-_expand_i",
"-Xcomp"
})
public int expandIntSoftware() {
return Integer.expand(intValue, intMask);
}
@Benchmark
@Fork(value = 2, jvmArgsAppend = {
"-XX:+UnlockDiagnosticVMOptions",
"-XX:ControlIntrinsic=+_expand_i",
"-Xcomp"
})
public int expandIntIntrinsic() {
return Integer.expand(intValue, intMask);
}
}
```
Here are the results on an i7 9700K, which supports the BMI2 instruction
set and is not affected by this issue:
```
Benchmark Mode Cnt Score Error
Units
PextPdepPerformanceBug.compressIntIntrinsic avgt 10 0,545 ± 0,002
ns/op
PextPdepPerformanceBug.compressIntSoftware avgt 10 11,357 ± 0,033
ns/op
PextPdepPerformanceBug.compressLongIntrinsic avgt 10 0,552 ± 0,012
ns/op
PextPdepPerformanceBug.compressLongSoftware avgt 10 16,197 ± 0,203
ns/op
PextPdepPerformanceBug.expandIntIntrinsic avgt 10 0,546 ± 0,006
ns/op
PextPdepPerformanceBug.expandIntSoftware avgt 10 12,179 ± 0,457
ns/op
PextPdepPerformanceBug.expandLongIntrinsic avgt 10 0,548 ± 0,018
ns/op
PextPdepPerformanceBug.expandLongSoftware avgt 10 17,658 ± 0,534
ns/op
```
And here are the results on a Ryzen 7 2700, which supports the BMI2
instruction set. but is also affected by this issue:
```
Benchmark Mode Cnt Score Error
Units
PextPdepPerformanceBug.compressIntIntrinsic avgt 10 28.010 ± 9.929
ns/op
PextPdepPerformanceBug.compressIntSoftware avgt 10 20.008 ± 2.129
ns/op
PextPdepPerformanceBug.compressLongIntrinsic avgt 10 48.999 ± 8.468
ns/op
PextPdepPerformanceBug.compressLongSoftware avgt 10 28.638 ± 5.336
ns/op
PextPdepPerformanceBug.expandIntIntrinsic avgt 10 24.860 ± 6.784
ns/op
PextPdepPerformanceBug.expandIntSoftware avgt 10 19.277 ± 1.719
ns/op
PextPdepPerformanceBug.expandLongIntrinsic avgt 10 43.889 ± 10.575
ns/op
PextPdepPerformanceBug.expandLongSoftware avgt 10 27.350 ± 1.898
ns/op
```
**Precedent and Scope**
A similar issue was reported in JDK-8334474 [4], where the compress/expand
intrinsics were disabled on RISC-V because the vectorized implementation
caused regressions compared to the pure-Java fallback.
This led me to investigate whether other JDK intrinsics relying on BMI2
instructions might be affected.
The good news is that, as stated before, PEXT and PDEP are the only BMI2
instructions that AMD implemented via microcode on pre-Zen 3 processors:
the others execute efficiently on all BMI2-capable hardware.
I also verified that no other JDK methods use PEXT/PDEP, so the four
methods covered in this report (Long.compress, Long.expand,
Integer.compress, Integer.expand) should be the only ones affected.
It's worth verifying this though as the JDK is very large and I could have
missed such examples.
**Mitigation**
The intrinsic selection logic should check both BMI2 support and CPU
vendor/family.
Specifically, disable these intrinsics when the CPU vendor is AMD and the
family is less than 0x19 (Zen 3).
I think this could be implemented in x86.ad [5], alongside the existing
BMI2 check, but I'm not familiar with C2's source code.
Still, I would be happy to work on this issue myself if the issue is
verified and it's acceptable for me to work on it.
Thanks for reading!
[1] https://bugs.openjdk.org/browse/JDK-8283893
[2] https://developer.amd.com/resources/developer-guides-manuals/
[3] https://en.wikipedia.org/wiki/X86_Bit_manipulation_instruction_set
[4] https://bugs.openjdk.org/browse/JDK-8334474
[5]
https://github.com/jatin-bhateja/jdk/blob/7d35a283cf2497565d230e3d5426f563f7e5870d/src/hotspot/cpu/x86/x86.ad#L3183
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/hotspot-compiler-dev/attachments/20251130/4d7bd0cf/attachment-0001.htm>
More information about the hotspot-compiler-dev
mailing list