mirror of
https://github.com/openjdk/jdk.git
synced 2026-06-10 12:37:09 +00:00
This change improves the AArch64 implementation of String.equals by
introducing SIMD-based fast paths using SVE and NEON.
SVE implementation:
- Uses predicated loads and comparisons for short lengths (len < VL)
- Uses a full predicated loop for longer inputs
- Handles the tail via an overlapped compare at (base + len - VL)
NEON implementation:
- Uses an 8-byte pre-read to simplify tail handling and eliminate
4/2/1-byte scalar branches
- Processes 16-byte chunks using LDP pair loads
- Uses CMP/CCMP to collapse comparisons into a single branch on mismatch
These changes reduce branch pressure and improve throughput for both
short and long strings.
Correctness:
- The implementation preserves existing semantics and matches behavior
for all lengths
Testing:
- Updated and extended intrinsic tests to cover boundary conditions
and mismatch positions
Benchmark:
Across evaluated macrobenchmarks (DaCapo and Renaissance), most workloads
spend <0.5% of CPU time in String.equals. DaCapo biojava is a notable
exception (~8–9%). In biojava, most String.equals calls are on very short
strings (1–2 bytes), where SVE shows ~1% end-to-end improvement, while
NEON is largely neutral or shows a small regression (~1%).
Measured using JMH on AArch64 (Arm Neoverse V2 CPU).
Values are relative (%) vs baseline. Negative values indicate regressions.
Mismatch results are reported across first(DF), middle(DM),
and last(DL) difference positions.
SVE results:
Length | L1_EQ L1_DF L1_DM L1_DL | U16_EQ U16_DF U16_DM U16_DL | Avg
-------+----------------------------+-----------------------------+------
0 | 19.63 | 20.05 | 19.84
1 | 16.59 17.81 16.57 18.34 | 16.02 0.71 0.42 1.39 | 10.98
2 | 16.44 1.32 0.30 -0.16 | 15.90 -5.17 -4.55 -1.09 | 2.87
3 | 26.58 1.60 1.43 27.07 | 30.34 -8.86 -7.06 14.08 | 10.65
7 | 41.47 -2.94 -3.37 39.82 | 24.02 -8.82 -6.27 20.48 | 13.05
8 | 19.08 -1.16 -3.50 -0.90 | 22.49 -9.75 17.50 13.13 | 7.11
9 | 20.17 -4.12 -5.17 19.03 | 9.25 -2.24 21.35 3.39 | 7.71
15 | 19.48 -3.83 -4.50 19.01 | 29.26 -10.06 11.76 17.07 | 9.77
16 | 19.04 -3.15 16.41 16.85 | 38.37 -11.12 13.18 27.70 | 14.66
17 | 8.95 -2.40 5.68 6.38 | 16.32 -1.61 7.49 11.44 | 6.53
31 | 28.87 -0.01 19.79 23.37 | 41.43 -7.57 23.85 35.89 | 20.70
32 | 32.58 3.38 12.39 26.90 | 46.01 -10.99 20.53 44.15 | 21.87
33 | 11.62 -15.20 6.04 13.27 | 32.27 -9.38 20.33 32.28 | 11.40
63 | 44.66 -11.59 37.20 42.56 | 55.41 -10.57 43.19 55.90 | 32.10
64 | 53.99 -2.19 27.04 51.79 | 59.36 -8.72 35.41 60.32 | 34.63
65 | 33.79 -14.01 23.95 29.15 | 48.91 -11.58 36.54 50.03 | 24.60
127 | 62.10 -3.79 47.51 62.79 | 58.13 -8.89 60.68 60.90 | 42.43
128 | 67.38 -2.47 38.62 67.09 | 62.83 -0.38 51.72 61.87 | 43.33
129 | 52.02 -1.42 39.17 49.20 | 55.04 -9.52 53.23 52.81 | 36.32
256 | 66.11 -1.38 56.12 64.93 | 70.67 -3.68 53.67 74.54 | 47.62
Average:
33.03 -2.40 17.46 30.34 | 37.60 -7.27 23.84 33.49 | 20.91
NEON results:
Length | L1_EQ L1_DF L1_DM L1_DL | U16_EQ U16_DF U16_DM U16_DL | Avg
-------+----------------------------+-----------------------------+------
0 | 9.22 | 8.69 | 8.95
1 | 3.07 3.59 1.34 5.42 | 6.36 -6.20 -6.71 -10.59 | -0.47
2 | 3.23 -4.79 -5.67 -4.09 | 8.06 -8.43 -9.89 -9.20 | -3.85
3 | 12.80 -4.16 -3.95 11.28 | 11.94 -14.50 -14.41 11.83 | 1.36
7 | 31.00 -7.21 -12.76 33.59 | 4.73 -17.67 -17.38 1.65 | 1.99
8 | 4.43 -7.20 -4.70 -6.73 | 2.71 -18.05 -3.17 -4.05 | -4.59
9 | -9.33 -19.90 -16.27 -1.80 | 16.65 -23.72 4.26 8.78 | -5.17
15 | -6.96 -16.17 -15.60 -4.01 | 7.46 -24.60 -3.19 77.82 | 1.84
16 | 2.48 -16.38 -2.56 -3.62 | 9.08 -19.29 -5.45 77.93 | 5.27
17 | 4.88 -18.85 -0.18 19.35 | 18.43 -19.80 -8.37 84.96 | 10.05
31 | 6.92 -21.13 -4.62 60.71 | 24.42 -21.81 9.48 188.59 | 30.32
32 | 7.75 -24.20 -5.29 68.23 | 25.33 -20.57 4.17 183.65 | 29.88
33 | 20.23 -20.42 -11.33 98.60 | 23.76 -24.76 5.97 188.57 | 35.08
63 | 30.25 -22.30 14.29 152.37 | 25.02 -28.37 21.43 419.68 | 76.55
64 | 28.99 -22.91 9.03 185.51 | 38.20 -22.82 19.76 446.60 | 85.29
65 | 16.13 -21.77 1.45 211.38 | 27.94 -24.79 17.50 446.80 | 84.33
127 | 33.69 -28.94 28.75 429.23 | 41.75 -24.86 37.35 832.68 |168.71
128 | 26.28 -29.03 24.13 432.87 | 43.48 -18.53 26.44 810.20 |164.48
129 | 27.73 -20.30 20.84 439.01 | 44.09 -22.35 30.09 827.38 |168.31
256 | 53.30 -20.27 26.09 841.37 | 56.66 -21.07 47.41 1604.98|323.56
Average:
15.30 -16.97 2.26 156.24 | 22.24 -20.12 8.17 325.70 | 59.10
Observations:
- SVE shows consistent improvements across all tested lengths, with gains
increasing as input size grows
- NEON improves equal-string performance across all lengths
- NEON shows regressions for short mismatched inputs due to the loss
of the scalar tbz-based early-exit sequence, which efficiently
detects mismatches at small sizes and at early positions
- The scalar implementation relies on a branchy 4/2/1 tbz ladder,
which is efficient for early mismatches but suboptimal for equal
strings
- The NEON implementation replaces this with a branchless SIMD
approach and performs upfront comparisons of the first and last
8 bytes, improving throughput and late-mismatch detection
128 lines
4.3 KiB
Java
128 lines
4.3 KiB
Java
/*
|
|
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
|
|
* Copyright 2026 Arm Limited and/or its affiliates.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
package org.openjdk.bench.java.lang;
|
|
|
|
import org.openjdk.jmh.annotations.*;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/*
|
|
* This benchmark naively explores String::equals performance
|
|
*/
|
|
@BenchmarkMode(Mode.AverageTime)
|
|
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
|
@State(Scope.Thread)
|
|
@Warmup(iterations = 5, time = 1)
|
|
@Measurement(iterations = 5, time = 1)
|
|
@Fork(value = 3)
|
|
public class StringEquals {
|
|
|
|
public String test = new String("0123456789");
|
|
public String test2 = new String("tgntogjnrognagronagroangroarngorngaorng");
|
|
public String test3 = new String(test); // equal to test, but not same
|
|
public String test4 = new String("0123\u01FF");
|
|
public String test5 = new String(test4); // equal to test4, but not same
|
|
public String test6 = new String("0123456780");
|
|
public String test7 = new String("0123\u01FE");
|
|
// string with parameterizable size
|
|
public String test8;
|
|
// same chars as test8, but different object; forces the intrinsic to read
|
|
// the entire string to check equality
|
|
public String test9;
|
|
// same chars as test8, except at length + diff_pos; set diff_pos to the
|
|
// worst case for the intrinsic being tested (usually -1, but could be -9
|
|
// if the intrinsic reads the last 8B first, or -length if the intrinsic
|
|
// reads the string backwards
|
|
public String test10;
|
|
|
|
@Param({"30"}) // can be used at runtime to define a length sweep
|
|
public int size;
|
|
|
|
@Param({"-1"}) // set to the worst location for the intrinsic under test
|
|
public int diff_pos;
|
|
|
|
@Setup
|
|
public void setup() {
|
|
if(size > 0) {
|
|
test8 = "a".repeat(size);
|
|
// NOTE 1: can't do test9 = new String(test8) or they'll share byte
|
|
// arrays, which improves cache hit rate of the equal-string case
|
|
test9 = "a".repeat(size);
|
|
StringBuilder sb = new StringBuilder("a".repeat(size));
|
|
sb.setCharAt(Math.max(test8.length() + diff_pos, 0), 'b');
|
|
test10 = sb.toString();
|
|
}
|
|
else {
|
|
// NOTE 2: can't use "a".repeat(0) or it returns the "" literal,
|
|
// which will early-exit from String.equals()
|
|
// NOTE 3: can't use no-arg String ctor or they'll share the byte
|
|
// array of the "" literal, which improves cache hit rate for
|
|
// intrinsics that read backwards into the object header
|
|
test8 = new String(new char [] {});
|
|
test9 = new String(new char [] {});
|
|
test10 = new String(new char [] {});
|
|
}
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean different() {
|
|
return test.equals(test2);
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean equal() {
|
|
return test.equals(test3);
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean differentParam() {
|
|
return test8.equals(test10);
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean equalParam() {
|
|
return test8.equals(test9);
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean almostEqual() {
|
|
return test.equals(test6);
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean almostEqualUTF16() {
|
|
return test4.equals(test7);
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean differentCoders() {
|
|
return test.equals(test4);
|
|
}
|
|
|
|
@Benchmark
|
|
public boolean equalsUTF16() {
|
|
return test5.equals(test4);
|
|
}
|
|
}
|
|
|