Repository navigation
List iteration benchmark #10888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
gh-worker-dd-mergequeue-cf854d
merged 15 commits into
master
from
dougqh/list-iteration-benchmark
Jun 24, 2026
Merged
List iteration benchmark #10888
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
d2a2295
List iteration benchmark
dougqh 372dff0
Merge branch 'master' into dougqh/list-iteration-benchmark
dougqh 54883cd
Fixing silly oversight - commented out benchmarks to run one in isola…
dougqh d002783
A bit of clean-up
dougqh 0636a14
Merge branch 'master' into dougqh/list-iteration-benchmark
dougqh 27a33f0
spotless
dougqh b19d281
Merge branch 'dougqh/list-iteration-benchmark' of github.com:DataDog/…
dougqh 8056b56
Update internal-api/src/jmh/java/datadog/trace/util/ListIterationBenc…
dougqh b816fde
Update internal-api/src/jmh/java/datadog/trace/util/ListIterationBenc…
dougqh 1f0f4e5
Merge branch 'master' into dougqh/list-iteration-benchmark
sarahchen6 600cc9b
Update internal-api/src/jmh/java/datadog/trace/util/ListIterationBenc…
dougqh 26c2c0b
Isolate per-thread collections in ListIterationBenchmark
dougqh a981815
Merge remote-tracking branch 'origin/master' into dougqh/list-iterati…
dougqh 8571e72
Replace stale results in ListIterationBenchmark with Java 17 numbers
dougqh e67ff0a
Merge branch 'master' into dougqh/list-iteration-benchmark
dougqh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
204 changes: 204 additions & 0 deletions
204
internal-api/src/jmh/java/datadog/trace/util/ListIterationBenchmark.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| package datadog.trace.util; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.Iterator; | ||
| import java.util.List; | ||
| import java.util.function.Supplier; | ||
| import org.openjdk.jmh.annotations.Benchmark; | ||
| import org.openjdk.jmh.annotations.CompilerControl; | ||
| import org.openjdk.jmh.annotations.CompilerControl.Mode; | ||
| import org.openjdk.jmh.annotations.Fork; | ||
| import org.openjdk.jmh.annotations.Level; | ||
| import org.openjdk.jmh.annotations.Measurement; | ||
| import org.openjdk.jmh.annotations.Param; | ||
| import org.openjdk.jmh.annotations.Scope; | ||
| import org.openjdk.jmh.annotations.Setup; | ||
| import org.openjdk.jmh.annotations.State; | ||
| import org.openjdk.jmh.annotations.Threads; | ||
| import org.openjdk.jmh.annotations.Warmup; | ||
|
|
||
| /** | ||
| * Benchmark comparing different ways to iterate list of different types and sizes -- both with | ||
| * simple loop bodies (inline case) and complicated loop bodies (dont inline case). | ||
| * | ||
| * <ul> | ||
| * Compares... | ||
| * <li>(RECOMMENDED) enhanced for loop / iterator - usually most performant, since escape analysis | ||
| * usually eliminates iterator allocation | ||
| * <li>(SITUATIONAL) List.forEach - good when using a non-capturing lambda, when escape analysis | ||
| * fails to eliminate iterator allocation - good alternative | ||
| * <li>(SITUATIONAL) c-style i=0; i < list.size() - usually worse than enhanced for - might be | ||
| * useful with complicated loop body when escape analysis fails to eliminate the iterator | ||
| * <li>(DISCOURAGED) List.stream - always incurs allocation overhead - usually unnecessary | ||
| * <li>(DISCOURAGED) List.parallelStream - heavy allocation overhead - only beneficial when | ||
| * working with sets (uncommon in the java agent) | ||
| * <li> | ||
| * </ul> | ||
| * | ||
| * <p>Java 17 results (Apple M1, {@code @Fork(2)}, {@code @Threads(8)}; {@code ArrayList}, M ops/s = | ||
| * millions, shown at two sizes): <code> | ||
| * Iteration style size 10 size 100 | ||
| * cstyleFor 1050 165 (fastest) | ||
| * forEach 995 163 | ||
| * enhancedFor 945 153 | ||
| * iterator 935 148 (noisier run-to-run) | ||
| * streams 158 45 (~3.6x slower; allocates) | ||
| * parallelStreams ~1 ~0.3 (catastrophic at these sizes) | ||
| * </code> | ||
| * | ||
| * <p>Key findings: | ||
| * | ||
| * <ul> | ||
| * <li>For {@code ArrayList}, the direct styles -- {@code cstyleFor}, {@code forEach}, | ||
| * enhanced-for, and explicit {@code iterator} -- cluster within ~10% of each other; escape | ||
| * analysis eliminates the iterator allocation, so enhanced-for/iterator stay competitive | ||
| * while reading cleanest (the RECOMMENDED choice). | ||
| * <li>{@code stream()} is ~3.6x slower than direct iteration and allocates per call -- avoid on | ||
| * hot paths. | ||
| * <li>{@code parallelStream()} is catastrophic for small collections (hundreds of times slower): | ||
| * ForkJoinPool split/coordinate overhead dwarfs the work, and it is run-to-run erratic. Never | ||
| * use it for the small lists typical in the agent. | ||
| * <li>{@code _inline} vs {@code _dont_inline} loop bodies barely differ at these sizes -- the | ||
| * iteration mechanics dominate, not the body. | ||
| * </ul> | ||
| */ | ||
| @Fork(2) | ||
| @Warmup(iterations = 2) | ||
| @Measurement(iterations = 3) | ||
| @Threads(8) | ||
| @State(Scope.Thread) | ||
| public class ListIterationBenchmark { | ||
| public static final class Element { | ||
| int num = 0; | ||
|
|
||
| @CompilerControl(Mode.INLINE) | ||
| void manipulate_inline() { | ||
| this.num += 1; | ||
| } | ||
|
|
||
| @CompilerControl(Mode.DONT_INLINE) | ||
| void manipulate_dont_inline() { | ||
| this.num += 1; | ||
| } | ||
| } | ||
|
|
||
| static ArrayList<Element> newArrayList(int size) { | ||
| ArrayList<Element> newList = new ArrayList<>(size); | ||
| for (int i = 0; i < size; ++i) { | ||
| newList.add(new Element()); | ||
| } | ||
| return newList; | ||
| } | ||
|
|
||
| /** | ||
| * Describes the list under test as a factory rather than a prebuilt instance. Each benchmark | ||
| * thread builds its own list (with its own {@link Element}s) in {@link #setUp()}, so the {@code | ||
| * manipulate_*} mutations stay thread-local — otherwise, with {@code @Threads(8)} sharing one | ||
| * list held in an enum constant, the benchmark would measure cross-thread contention on {@code | ||
| * Element.num} rather than iteration cost. | ||
| */ | ||
| public enum ListSpec { | ||
| COLLECTIONS_EMPTY_LIST(Collections::emptyList), | ||
| EMPTY_ARRAY_LIST(ArrayList::new), | ||
| SINGLETON_LIST(() -> Collections.singletonList(new Element())), | ||
| ARRAY_LIST_1(() -> newArrayList(1)), | ||
| ARRAY_LIST_5(() -> newArrayList(5)), | ||
| ARRAY_LIST_10(() -> newArrayList(10)), | ||
| ARRAY_LIST_100(() -> newArrayList(100)); | ||
|
|
||
| private final Supplier<List<Element>> factory; | ||
|
|
||
| ListSpec(Supplier<List<Element>> factory) { | ||
| this.factory = factory; | ||
| } | ||
|
|
||
| List<Element> build() { | ||
| return factory.get(); | ||
| } | ||
| } | ||
|
|
||
| @Param ListSpec listSpec; | ||
|
|
||
| List<Element> list; | ||
|
|
||
| @Setup(Level.Trial) | ||
| public void setUp() { | ||
| // Built per thread (the class is @State(Scope.Thread)) so each thread owns its own Elements. | ||
| this.list = this.listSpec.build(); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void forEach_inline() { | ||
| this.list.forEach(Element::manipulate_inline); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void forEach_dont_inline() { | ||
| this.list.forEach(Element::manipulate_dont_inline); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void enhancedFor_inline() { | ||
| // Enhanced for-loop is just syntax sugar for an Iterator | ||
| for (Element e : this.list) { | ||
| e.manipulate_inline(); | ||
| } | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void enhancedFor_dont_inline() { | ||
| // Enhanced for-loop is just syntax sugar for an Iterator | ||
| for (Element e : this.list) { | ||
| e.manipulate_dont_inline(); | ||
| } | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void iterator_inline() { | ||
| for (Iterator<Element> iter = this.list.iterator(); iter.hasNext(); ) { | ||
| iter.next().manipulate_inline(); | ||
| } | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void iterator_dont_inline() { | ||
| for (Iterator<Element> iter = this.list.iterator(); iter.hasNext(); ) { | ||
| iter.next().manipulate_dont_inline(); | ||
| } | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void cstyleFor_inline() { | ||
| for (int i = 0; i < this.list.size(); ++i) { | ||
| this.list.get(i).manipulate_inline(); | ||
| } | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void cstyleFor_dont_inline() { | ||
| for (int i = 0; i < this.list.size(); ++i) { | ||
| this.list.get(i).manipulate_dont_inline(); | ||
| } | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void streams_inline() { | ||
| this.list.stream().forEach(Element::manipulate_inline); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void streams_dont_inline() { | ||
| this.list.stream().forEach(Element::manipulate_dont_inline); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void parallelStreams_inline() { | ||
| this.list.parallelStream().forEach(Element::manipulate_inline); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void parallelStreams_dont_inline() { | ||
| this.list.parallelStream().forEach(Element::manipulate_dont_inline); | ||
| } | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.