Skip to content
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

LANG-1675 - Improve performance of StringUtils.join for primitives #812

Merged

Conversation

HubertWo
Copy link
Contributor

@HubertWo HubertWo commented Oct 9, 2021

This PR replaces StringJoiner by StringBuilder in StringUtils.join methods.
The change is limited to methods which use primitives as input only.

More

Please find more details here:

  1. https://issues.apache.org/jira/browse/LANG-1675
  2. [LANG-1593] Common behavior for StringUtils join APIs when called with char or String delimiter #784

JMH with example results

JMH tests - StringBuilder vs StringJoiner may be found in dedicated repository. Full logs, code and results included.
https://github.com/HubertWo/apache-commons-lang-jmh

@coveralls
Copy link

Coverage Status

Coverage increased (+0.01%) to 94.961% when pulling f2f6cd9 on HubertWo:fix/LANG-1675_string_join_refactor into a3a0645 on apache:master.

@jochenw
Copy link
Contributor

jochenw commented Oct 9, 2021 via email

@XenoAmess
Copy link
Contributor

XenoAmess commented Oct 9, 2021

Reading through this, I wonder whether we should deprecate StringJoiner?

agree.

@garydgregory
Copy link
Member

Really? How are you going to do that? It's part of the JDK...

@XenoAmess
Copy link
Contributor

Really? How are you going to do that? It's part of the JDK...

yep,that is the only thing stopping me from deprecate it lol

@HubertWo
Copy link
Contributor Author

HubertWo commented Oct 10, 2021

Reading through this, I wonder whether we should deprecate StringJoiner?

If the benchmarks are correct, the difference in performance is huge. I also did some research and looks like StringBuilder is much better choice in this case.

@garydgregory
Copy link
Member

What do the POMs in https://github.com/HubertWo/apache-commons-lang-jmh require Java 17? I'd be much easier to test on Java 8 without forcing these settings on to the tester by default.

@garydgregory
Copy link
Member

@HubertWo
Hm, I'm not sure how you ran your benchmarks, I had to hack the POM like we do in Commons IO to be able to run "mvn clean test".

May you please update the project to remove the Java 17 requirement and make the benchmarks runnable from the command line? I'd like to just run a Maven command (however you want to set it up) and then post my results here so we can compare results.
TY!

@HubertWo
Copy link
Contributor Author

HubertWo commented Nov 17, 2021

Hi @garydgregory

Thank you for your time.
I removed Java 17 dependencies. Now everything works agains Java 8.
Also it's possible to run benchmarks from command line - exactly the same as described in the official JMH doc.

Please find run instructions and example results here:

  1. https://github.com/HubertWo/apache-commons-lang-jmh/tree/main/string-joiner
  2. https://github.com/HubertWo/apache-commons-lang-jmh/tree/main/string-builder

I have also added GitHub Actions which run both benchmarks agains Java 8.

Logs from GitHub run:

StringJoinerBenchmark.stringJoinerPrimitiveInt      thrpt   25  7963210.479 ± 188742.938  ops/s
StringBuilderBenchmark.stringBuilderPrimitiveInt    thrpt   25  21906622.479 ± 950041.487  ops/s
  1. GitHub build logs for StringJoiner benchmark: https://github.com/HubertWo/apache-commons-lang-jmh/runs/4241475267?check_suite_focus=true
  2. GitHub build logs for StringBuilder benchmark: https://github.com/HubertWo/apache-commons-lang-jmh/runs/4242012768?check_suite_focus=true

@garydgregory
Copy link
Member

garydgregory commented Nov 17, 2021

Hi @HubertWo

Thank you for your update.

I just pulled from your repo and saw:

 create mode 100644 string-builder/snapshot/commons-lang3-3.13.0-SNAPSHOT.jar
 create mode 100644 string-builder/snapshot/commons-lang3-3.13.0-SNAPSHOT.pom

This is confusing to me. Why are these files here? I want the SNAPSHOT in my local Maven repository to be used. Otherwise, do I have to copy the jar and pom over to this snapshot folder every time I touch sources in Commons Lang?

@HubertWo
Copy link
Contributor Author

HubertWo commented Nov 18, 2021

JAR was added only to run benchmarks on GitHub actions. (https://github.com/HubertWo/apache-commons-lang-jmh/blob/main/.github/workflows/maven.yml)

You may ignore both files.

@garydgregory
Copy link
Member

This is what I see on Microsoft Windows [Version 10.0.19042.1288], Java 8, and 11:

StringJoinerBenchmark.stringJoinerPrimitiveInt on Java 8

 8:29:24.04 C:\temp\apache-commons-lang-jmh\string-joiner>java -jar target/benchmarks.jar
# JMH version: 1.33
# VM version: JDK 1.8.0_312, OpenJDK 64-Bit Server VM, 25.312-b07
# VM invoker: C:\Program Files\Eclipse Adoptium\jdk-8.0.312.7-hotspot\jre\bin\java.exe
# VM options: <none>
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.github.hubertwo.acljmh.StringJoinerBenchmark.stringJoinerPrimitiveInt

# Run progress: 0.00% complete, ETA 00:08:20
# Fork: 1 of 5
# Warmup Iteration   1: 4028049.411 ops/s
# Warmup Iteration   2: 4760495.147 ops/s
# Warmup Iteration   3: 5272759.983 ops/s
# Warmup Iteration   4: 5217695.792 ops/s
# Warmup Iteration   5: 5281258.927 ops/s
Iteration   1: 5223688.819 ops/s
Iteration   2: 5818978.183 ops/s
Iteration   3: 5632986.395 ops/s
Iteration   4: 5972299.312 ops/s
Iteration   5: 5729786.303 ops/s

# Run progress: 20.00% complete, ETA 00:06:43
# Fork: 2 of 5
# Warmup Iteration   1: 5754531.770 ops/s
# Warmup Iteration   2: 6204029.918 ops/s
# Warmup Iteration   3: 5997509.994 ops/s
# Warmup Iteration   4: 6068735.288 ops/s
# Warmup Iteration   5: 6259803.017 ops/s
Iteration   1: 6097840.957 ops/s
Iteration   2: 5893229.379 ops/s
Iteration   3: 6310146.092 ops/s
Iteration   4: 6137171.976 ops/s
Iteration   5: 5792914.747 ops/s

# Run progress: 40.00% complete, ETA 00:05:02
# Fork: 3 of 5
# Warmup Iteration   1: 6154276.895 ops/s
# Warmup Iteration   2: 6103484.534 ops/s
# Warmup Iteration   3: 6218572.312 ops/s
# Warmup Iteration   4: 6004638.320 ops/s
# Warmup Iteration   5: 6268515.946 ops/s
Iteration   1: 6133699.105 ops/s
Iteration   2: 5754235.565 ops/s
Iteration   3: 6157983.152 ops/s
Iteration   4: 6049810.564 ops/s
Iteration   5: 6281478.040 ops/s

# Run progress: 60.00% complete, ETA 00:03:21
# Fork: 4 of 5
# Warmup Iteration   1: 6142365.003 ops/s
# Warmup Iteration   2: 6397104.178 ops/s
# Warmup Iteration   3: 6243445.936 ops/s
# Warmup Iteration   4: 6309976.325 ops/s
# Warmup Iteration   5: 6190286.103 ops/s
Iteration   1: 6503690.619 ops/s
Iteration   2: 6381886.335 ops/s
Iteration   3: 6218666.379 ops/s
Iteration   4: 6429726.336 ops/s
Iteration   5: 6123348.392 ops/s

# Run progress: 80.00% complete, ETA 00:01:40
# Fork: 5 of 5
# Warmup Iteration   1: 6219247.906 ops/s
# Warmup Iteration   2: 6613614.639 ops/s
# Warmup Iteration   3: 6578974.636 ops/s
# Warmup Iteration   4: 6380279.170 ops/s
# Warmup Iteration   5: 6555631.361 ops/s
Iteration   1: 6459206.567 ops/s
Iteration   2: 6399048.111 ops/s
Iteration   3: 6573701.097 ops/s
Iteration   4: 6561843.207 ops/s
Iteration   5: 6490106.995 ops/s


Result "com.github.hubertwo.acljmh.StringJoinerBenchmark.stringJoinerPrimitiveInt":
  6125098.905 ±(99.9%) 252123.433 ops/s [Average]
  (min, avg, max) = (5223688.819, 6125098.905, 6573701.097), stdev = 336577.570
  CI (99.9%): [5872975.472, 6377222.338] (assumes normal distribution)


# Run complete. Total time: 00:08:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                        Mode  Cnt        Score        Error  Units
StringJoinerBenchmark.stringJoinerPrimitiveInt  thrpt   25  6125098.905 ± 252123.433  ops/s

StringBuilderBenchmark.stringBuilderPrimitiveInt on Java 8

 8:41:58.46 C:\temp\apache-commons-lang-jmh\string-builder>java -jar target/benchmarks.jar
# JMH version: 1.33
# VM version: JDK 1.8.0_312, OpenJDK 64-Bit Server VM, 25.312-b07
# VM invoker: C:\Program Files\Eclipse Adoptium\jdk-8.0.312.7-hotspot\jre\bin\java.exe
# VM options: <none>
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.github.hubertwo.acljmh.StringBuilderBenchmark.stringBuilderPrimitiveInt

# Run progress: 0.00% complete, ETA 00:08:20
# Fork: 1 of 5
# Warmup Iteration   1: 5638828.105 ops/s
# Warmup Iteration   2: 6012329.374 ops/s
# Warmup Iteration   3: 6028870.456 ops/s
# Warmup Iteration   4: 6378532.707 ops/s
# Warmup Iteration   5: 6415691.661 ops/s
Iteration   1: 6402935.472 ops/s
Iteration   2: 6403059.626 ops/s
Iteration   3: 6281448.263 ops/s
Iteration   4: 6485849.516 ops/s
Iteration   5: 6413346.541 ops/s

# Run progress: 20.00% complete, ETA 00:06:42
# Fork: 2 of 5
# Warmup Iteration   1: 5940727.783 ops/s
# Warmup Iteration   2: 6298039.536 ops/s
# Warmup Iteration   3: 6128525.034 ops/s
# Warmup Iteration   4: 6255026.777 ops/s
# Warmup Iteration   5: 5969974.958 ops/s
Iteration   1: 6240497.474 ops/s
Iteration   2: 6339066.113 ops/s
Iteration   3: 6246787.751 ops/s
Iteration   4: 6191611.372 ops/s
Iteration   5: 5393780.839 ops/s

# Run progress: 40.00% complete, ETA 00:05:02
# Fork: 3 of 5
# Warmup Iteration   1: 6292573.853 ops/s
# Warmup Iteration   2: 6550340.320 ops/s
# Warmup Iteration   3: 6586276.662 ops/s
# Warmup Iteration   4: 6475453.557 ops/s
# Warmup Iteration   5: 6445092.592 ops/s
Iteration   1: 6551636.499 ops/s
Iteration   2: 6494809.307 ops/s
Iteration   3: 6443369.421 ops/s
Iteration   4: 6437411.692 ops/s
Iteration   5: 6419118.459 ops/s

# Run progress: 60.00% complete, ETA 00:03:21
# Fork: 4 of 5
# Warmup Iteration   1: 5838425.211 ops/s
# Warmup Iteration   2: 6051767.339 ops/s
# Warmup Iteration   3: 6295104.534 ops/s
# Warmup Iteration   4: 6222992.137 ops/s
# Warmup Iteration   5: 6173991.713 ops/s
Iteration   1: 6236300.996 ops/s
Iteration   2: 6275138.569 ops/s
Iteration   3: 5890347.878 ops/s
Iteration   4: 6263455.340 ops/s
Iteration   5: 6335664.159 ops/s

# Run progress: 80.00% complete, ETA 00:01:40
# Fork: 5 of 5
# Warmup Iteration   1: 6141810.962 ops/s
# Warmup Iteration   2: 6396032.327 ops/s
# Warmup Iteration   3: 6360946.084 ops/s
# Warmup Iteration   4: 6324751.367 ops/s
# Warmup Iteration   5: 6273036.646 ops/s
Iteration   1: 6421965.689 ops/s
Iteration   2: 6508246.906 ops/s
Iteration   3: 6402817.891 ops/s
Iteration   4: 6146288.108 ops/s
Iteration   5: 6358559.337 ops/s


Result "com.github.hubertwo.acljmh.StringBuilderBenchmark.stringBuilderPrimitiveInt":
  6303340.529 ±(99.9%) 176697.569 ops/s [Average]
  (min, avg, max) = (5393780.839, 6303340.529, 6551636.499), stdev = 235886.199
  CI (99.9%): [6126642.960, 6480038.097] (assumes normal distribution)


# Run complete. Total time: 00:08:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                          Mode  Cnt        Score        Error  Units
StringBuilderBenchmark.stringBuilderPrimitiveInt  thrpt   25  6303340.529 ± 176697.569  ops/s

To summarize Java 8:

Benchmark                                        Mode  Cnt        Score        Error  Units
StringJoinerBenchmark.stringJoinerPrimitiveInt    thrpt   25  6125098.905 ± 252123.433  ops/s
StringBuilderBenchmark.stringBuilderPrimitiveInt  thrpt   25  6303340.529 ± 176697.569  ops/s

StringJoinerBenchmark.stringJoinerPrimitiveInt on Java 11

 9:01:02.35 C:\temp\apache-commons-lang-jmh\string-joiner>java -jar target/benchmarks.jar
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.openjdk.jmh.util.Utils (file:/C:/temp/apache-commons-lang-jmh/string-joiner/target/benchmarks.jar) to method java.io.Console.encoding()
WARNING: Please consider reporting this to the maintainers of org.openjdk.jmh.util.Utils
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
# JMH version: 1.33
# VM version: JDK 11.0.13, OpenJDK 64-Bit Server VM, 11.0.13+8
# VM invoker: C:\Program Files\Eclipse Adoptium\jdk-11.0.13.8-hotspot\bin\java.exe
# VM options: <none>
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.github.hubertwo.acljmh.StringJoinerBenchmark.stringJoinerPrimitiveInt

# Run progress: 0.00% complete, ETA 00:08:20
# Fork: 1 of 5
# Warmup Iteration   1: 6211311.992 ops/s
# Warmup Iteration   2: 6747262.431 ops/s
# Warmup Iteration   3: 6558912.817 ops/s
# Warmup Iteration   4: 6771742.789 ops/s
# Warmup Iteration   5: 6763107.701 ops/s
Iteration   1: 6798738.867 ops/s
Iteration   2: 6777925.203 ops/s
Iteration   3: 6647225.301 ops/s
Iteration   4: 6614494.453 ops/s
Iteration   5: 6737617.471 ops/s

# Run progress: 20.00% complete, ETA 00:06:43
# Fork: 2 of 5
# Warmup Iteration   1: 6459452.620 ops/s
# Warmup Iteration   2: 6382410.985 ops/s
# Warmup Iteration   3: 6489995.342 ops/s
# Warmup Iteration   4: 6393524.885 ops/s
# Warmup Iteration   5: 5875825.717 ops/s
Iteration   1: 5822396.839 ops/s
Iteration   2: 6413539.391 ops/s
Iteration   3: 6401168.928 ops/s
Iteration   4: 6473265.494 ops/s
Iteration   5: 6546418.900 ops/s

# Run progress: 40.00% complete, ETA 00:05:02
# Fork: 3 of 5
# Warmup Iteration   1: 6374389.170 ops/s
# Warmup Iteration   2: 6450623.105 ops/s
# Warmup Iteration   3: 6700384.526 ops/s
# Warmup Iteration   4: 5818958.017 ops/s
# Warmup Iteration   5: 5864596.372 ops/s
Iteration   1: 6615271.944 ops/s
Iteration   2: 6571880.374 ops/s
Iteration   3: 6654752.846 ops/s
Iteration   4: 6549549.943 ops/s
Iteration   5: 6599145.093 ops/s

# Run progress: 60.00% complete, ETA 00:03:21
# Fork: 4 of 5
# Warmup Iteration   1: 6473242.760 ops/s
# Warmup Iteration   2: 6379801.578 ops/s
# Warmup Iteration   3: 6252289.638 ops/s
# Warmup Iteration   4: 6505810.812 ops/s
# Warmup Iteration   5: 6505187.390 ops/s
Iteration   1: 6525028.117 ops/s
Iteration   2: 6489421.325 ops/s
Iteration   3: 6175421.960 ops/s
Iteration   4: 6322478.938 ops/s
Iteration   5: 6407571.523 ops/s

# Run progress: 80.00% complete, ETA 00:01:40
# Fork: 5 of 5
# Warmup Iteration   1: 6368889.086 ops/s
# Warmup Iteration   2: 6545990.921 ops/s
# Warmup Iteration   3: 6492715.633 ops/s
# Warmup Iteration   4: 6610368.781 ops/s
# Warmup Iteration   5: 6143078.704 ops/s
Iteration   1: 6742870.379 ops/s
Iteration   2: 6709030.111 ops/s
Iteration   3: 6703347.856 ops/s
Iteration   4: 6603775.783 ops/s
Iteration   5: 6656342.851 ops/s


Result "com.github.hubertwo.acljmh.StringJoinerBenchmark.stringJoinerPrimitiveInt":
  6542347.196 ±(99.9%) 158331.625 ops/s [Average]
  (min, avg, max) = (5822396.839, 6542347.196, 6798738.867), stdev = 211368.189
  CI (99.9%): [6384015.571, 6700678.820] (assumes normal distribution)


# Run complete. Total time: 00:08:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                        Mode  Cnt        Score        Error  Units
StringJoinerBenchmark.stringJoinerPrimitiveInt  thrpt   25  6542347.196 ± 158331.625  ops/s

StringBuilderBenchmark.stringBuilderPrimitiveInt on Java 11

 9:09:36.63 C:\temp\apache-commons-lang-jmh\string-joiner>cd ..\string-builder

 9:10:05.99 C:\temp\apache-commons-lang-jmh\string-builder>java -jar target/benchmarks.jar
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.openjdk.jmh.util.Utils (file:/C:/temp/apache-commons-lang-jmh/string-builder/target/benchmarks.jar) to method java.io.Console.encoding()
WARNING: Please consider reporting this to the maintainers of org.openjdk.jmh.util.Utils
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
# JMH version: 1.33
# VM version: JDK 11.0.13, OpenJDK 64-Bit Server VM, 11.0.13+8
# VM invoker: C:\Program Files\Eclipse Adoptium\jdk-11.0.13.8-hotspot\bin\java.exe
# VM options: <none>
# Blackhole mode: full + dont-inline hint (default, use -Djmh.blackhole.autoDetect=true to auto-detect)
# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.github.hubertwo.acljmh.StringBuilderBenchmark.stringBuilderPrimitiveInt

# Run progress: 0.00% complete, ETA 00:08:20
# Fork: 1 of 5
# Warmup Iteration   1: 6124265.805 ops/s
# Warmup Iteration   2: 6280599.406 ops/s
# Warmup Iteration   3: 5537584.025 ops/s
# Warmup Iteration   4: 5442440.575 ops/s
# Warmup Iteration   5: 6005466.091 ops/s
Iteration   1: 6303651.443 ops/s
Iteration   2: 6351930.132 ops/s
Iteration   3: 6462915.675 ops/s
Iteration   4: 6261262.701 ops/s
Iteration   5: 4764485.626 ops/s

# Run progress: 20.00% complete, ETA 00:06:43
# Fork: 2 of 5
# Warmup Iteration   1: 5610674.138 ops/s
# Warmup Iteration   2: 5716158.382 ops/s
# Warmup Iteration   3: 6387316.364 ops/s
# Warmup Iteration   4: 6389239.939 ops/s
# Warmup Iteration   5: 6230817.570 ops/s
Iteration   1: 6294865.152 ops/s
Iteration   2: 6487501.018 ops/s
Iteration   3: 6385049.433 ops/s
Iteration   4: 6332667.452 ops/s
Iteration   5: 6395522.434 ops/s

# Run progress: 40.00% complete, ETA 00:05:02
# Fork: 3 of 5
# Warmup Iteration   1: 6501740.764 ops/s
# Warmup Iteration   2: 6245764.152 ops/s
# Warmup Iteration   3: 6325628.565 ops/s
# Warmup Iteration   4: 6362356.744 ops/s
# Warmup Iteration   5: 6445932.539 ops/s
Iteration   1: 6317627.507 ops/s
Iteration   2: 6415113.085 ops/s
Iteration   3: 6368157.935 ops/s
Iteration   4: 6132464.782 ops/s
Iteration   5: 6418124.475 ops/s

# Run progress: 60.00% complete, ETA 00:03:21
# Fork: 4 of 5
# Warmup Iteration   1: 5816353.649 ops/s
# Warmup Iteration   2: 6271228.168 ops/s
# Warmup Iteration   3: 5020208.787 ops/s
# Warmup Iteration   4: 5472176.864 ops/s
# Warmup Iteration   5: 6339575.933 ops/s
Iteration   1: 6448960.953 ops/s
Iteration   2: 6415139.122 ops/s
Iteration   3: 6352952.242 ops/s
Iteration   4: 6206598.587 ops/s
Iteration   5: 6316547.353 ops/s

# Run progress: 80.00% complete, ETA 00:01:40
# Fork: 5 of 5
# Warmup Iteration   1: 6017145.794 ops/s
# Warmup Iteration   2: 5962584.069 ops/s
# Warmup Iteration   3: 5923748.905 ops/s
# Warmup Iteration   4: 5431951.901 ops/s
# Warmup Iteration   5: 5939084.630 ops/s
Iteration   1: 5953082.874 ops/s
Iteration   2: 6310576.360 ops/s
Iteration   3: 6299812.394 ops/s
Iteration   4: 6253949.139 ops/s
Iteration   5: 6603114.825 ops/s


Result "com.github.hubertwo.acljmh.StringBuilderBenchmark.stringBuilderPrimitiveInt":
  6274082.908 ±(99.9%) 253394.583 ops/s [Average]
  (min, avg, max) = (4764485.626, 6274082.908, 6603114.825), stdev = 338274.518
  CI (99.9%): [6020688.325, 6527477.491] (assumes normal distribution)


# Run complete. Total time: 00:08:23

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                          Mode  Cnt        Score        Error  Units
StringBuilderBenchmark.stringBuilderPrimitiveInt  thrpt   25  6274082.908 ± 253394.583  ops/s

To summarize Java 11:

Benchmark                                          Mode  Cnt        Score        Error  Units
StringJoinerBenchmark.stringJoinerPrimitiveInt    thrpt   25  6542347.196 ± 158331.625  ops/s
StringBuilderBenchmark.stringBuilderPrimitiveInt  thrpt   25  6274082.908 ± 253394.583  ops/s

To summarize Java 8 and 11:

Benchmark                                          Mode  Cnt        Score        Error  Units
StringJoinerBenchmark.stringJoinerPrimitiveInt    Java 8 thrpt   25  6125098.905 ± 252123.433  ops/s
StringJoinerBenchmark.stringJoinerPrimitiveInt    Java 11 thrpt  25  6542347.196 ± 158331.625  ops/s
StringBuilderBenchmark.stringBuilderPrimitiveInt  Java 8 thrpt   25  6303340.529 ± 176697.569  ops/s
StringBuilderBenchmark.stringBuilderPrimitiveInt  Java 11 thrpt  25  6274082.908 ± 253394.583  ops/s

These results look mixed to me: The PR implementation overlaps the current code accounting for the margin of errors.

Can anyone reproduce the results? Or any result?

I did not run Java 17 since I won't be able to use it in production for a while.

Copy link
Member

@kinow kinow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look at the linked repository, cloned it, tried to run without success. Had another look at the code, and realized the dependencies in the pom.xml were used in Lang too. So I copied the StringJoiner test to the master branch and ran that test first.

My environment:

Apache Maven 3.8.2 (ea98e05a04480131370aa0c110b8c54cf726c06f)
Maven home: /opt/apache-maven-3.8.2
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-90-generic", arch: "amd64", family: "unix"

And class annotations I used in my tests.

@BenchmarkMode(Mode.Throughput) // Then saved again with .AverageTime to re-run tests
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)

StringJoiner + master

Using master on the following commit.

commit 7f9472e8989a859b7b4a3394486c7064cb3d11af (HEAD -> master, upstream/master, upstream/HEAD, dependabot/maven/com.puppycrawl.tools-checkstyle-9.1)
Author: Gary Gregory <[email protected]>
Date:   Thu Nov 18 22:12:49 2021 -0500

    Fix Javadoc typo.

First thing I did was to edit the pom.xml to run just that one test.

diff --git a/pom.xml b/pom.xml
index 021e056d9..b48d6c750 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1022,7 +1022,7 @@
       <id>benchmark</id>
       <properties>
         <skipTests>true</skipTests>
-        <benchmark>org.apache</benchmark>
+        <benchmark>org.apache.commons.lang3.StringJoinerBenchmarkTest</benchmark>
       </properties>
       <build>
         <plugins>

A copied the StringJoiner test editing package name, adding the license header, and adding more JMH annotations. First to run with average time, and then with the throughput.

Average time results:

(...)
Result "org.apache.commons.lang3.StringJoinerBenchmarkTest.stringJoinerPrimitiveBoolean":
  93.846 ±(99.9%) 6.002 ns/op [Average]
  (min, avg, max) = (84.979, 93.846, 112.687), stdev = 8.013
  CI (99.9%): [87.844, 99.848] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringJoinerBenchmarkTest.stringJoinerPrimitiveInt":
  110.469 ±(99.9%) 2.159 ns/op [Average]
  (min, avg, max) = (107.508, 110.469, 120.933), stdev = 2.883
  CI (99.9%): [108.310, 112.629] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringJoinerBenchmarkTest.stringJoinerPrimitiveLong":
  109.523 ±(99.9%) 1.101 ns/op [Average]
  (min, avg, max) = (107.619, 109.523, 111.960), stdev = 1.470
  CI (99.9%): [108.421, 110.624] (assumes normal distribution)

# Run complete. Total time: 00:25:05

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                               Mode  Cnt    Score   Error  Units
StringJoinerBenchmarkTest.stringJoinerPrimitiveBoolean  avgt   25   93.846 ± 6.002  ns/op
StringJoinerBenchmarkTest.stringJoinerPrimitiveInt      avgt   25  110.469 ± 2.159  ns/op
StringJoinerBenchmarkTest.stringJoinerPrimitiveLong     avgt   25  109.523 ± 1.101  ns/op

Throughput results:

(...)
Result "org.apache.commons.lang3.StringJoinerBenchmarkTest.stringJoinerPrimitiveBoolean":
  0.011 ±(99.9%) 0.001 ops/ns [Average]
  (min, avg, max) = (0.010, 0.011, 0.012), stdev = 0.001
  CI (99.9%): [0.011, 0.012] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringJoinerBenchmarkTest.stringJoinerPrimitiveInt":
  0.009 ±(99.9%) 0.001 ops/ns [Average]
  (min, avg, max) = (0.007, 0.009, 0.010), stdev = 0.001
  CI (99.9%): [0.008, 0.009] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringJoinerBenchmarkTest.stringJoinerPrimitiveLong":
  0.009 ±(99.9%) 0.001 ops/ns [Average]
  (min, avg, max) = (0.008, 0.009, 0.009), stdev = 0.001
  CI (99.9%): [0.008, 0.009] (assumes normal distribution)

# Run complete. Total time: 00:25:05

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                                Mode  Cnt  Score    Error   Units
StringJoinerBenchmarkTest.stringJoinerPrimitiveBoolean  thrpt   25  0.011 ±  0.001  ops/ns
StringJoinerBenchmarkTest.stringJoinerPrimitiveInt      thrpt   25  0.009 ±  0.001  ops/ns
StringJoinerBenchmarkTest.stringJoinerPrimitiveLong     thrpt   25  0.009 ±  0.001  ops/ns

StringBuilder + the branch for this pull request

Did the same thing using the branch for this pull request, but with the code for testing StringBuilder with JMH.

commit f2f6cd9b0580f25a3a5447da092ac30642abd2aa (HEAD -> pr-812)
Merge: 1fe3e64bc a3a064587
Author: Hubert <[email protected]>
Date:   Sat Oct 9 12:54:37 2021 +0200

    Merge branch 'apache:master' into fix/LANG-1675_string_join_refactor
diff --git a/pom.xml b/pom.xml
index 64e3e8d85..498e44f4f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1005,7 +1005,7 @@
       <id>benchmark</id>
       <properties>
         <skipTests>true</skipTests>
-        <benchmark>org.apache</benchmark>
+        <benchmark>org.apache.commons.lang3.StringBuilderBenchmarkTest</benchmark>
       </properties>
       <build>
         <plugins>

Average time results:

(...)
Result "org.apache.commons.lang3.StringBuilderBenchmarkTest.stringBuilderPrimitiveBoolean":
  49.887 ±(99.9%) 0.542 ns/op [Average]
  (min, avg, max) = (49.086, 49.887, 52.508), stdev = 0.723
  CI (99.9%): [49.345, 50.429] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringBuilderBenchmarkTest.stringBuilderPrimitiveInt":
  36.461 ±(99.9%) 0.505 ns/op [Average]
  (min, avg, max) = (35.892, 36.461, 37.850), stdev = 0.674
  CI (99.9%): [35.956, 36.966] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringBuilderBenchmarkTest.stringBuilderPrimitiveLong":
  37.222 ±(99.9%) 0.157 ns/op [Average]
  (min, avg, max) = (36.922, 37.222, 37.824), stdev = 0.209
  CI (99.9%): [37.065, 37.379] (assumes normal distribution)
(...)

# Run complete. Total time: 00:25:05

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                             Mode  Cnt   Score   Error  Units
StringBuilderBenchmarkTest.stringBuilderPrimitiveBoolean  avgt   25  49.887 ± 0.542  ns/op
StringBuilderBenchmarkTest.stringBuilderPrimitiveInt      avgt   25  36.461 ± 0.505  ns/op
StringBuilderBenchmarkTest.stringBuilderPrimitiveLong     avgt   25  37.222 ± 0.157  ns/op

Throughput results:

(...)
Result "org.apache.commons.lang3.StringBuilderBenchmarkTest.stringBuilderPrimitiveBoolean":
  0.020 ±(99.9%) 0.001 ops/ns [Average]
  (min, avg, max) = (0.020, 0.020, 0.020), stdev = 0.001
  CI (99.9%): [0.020, 0.020] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringBuilderBenchmarkTest.stringBuilderPrimitiveInt":
  0.026 ±(99.9%) 0.001 ops/ns [Average]
  (min, avg, max) = (0.024, 0.026, 0.028), stdev = 0.001
  CI (99.9%): [0.025, 0.027] (assumes normal distribution)
(...)
Result "org.apache.commons.lang3.StringBuilderBenchmarkTest.stringBuilderPrimitiveLong":
  0.026 ±(99.9%) 0.001 ops/ns [Average]
  (min, avg, max) = (0.025, 0.026, 0.027), stdev = 0.001
  CI (99.9%): [0.026, 0.026] (assumes normal distribution)

# Run complete. Total time: 00:25:05

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                              Mode  Cnt  Score    Error   Units
StringBuilderBenchmarkTest.stringBuilderPrimitiveBoolean  thrpt   25  0.020 ±  0.001  ops/ns
StringBuilderBenchmarkTest.stringBuilderPrimitiveInt      thrpt   25  0.026 ±  0.001  ops/ns
StringBuilderBenchmarkTest.stringBuilderPrimitiveLong     thrpt   25  0.026 ±  0.001  ops/ns

TL;DR:

Before

StringJoinerBenchmarkTest.stringJoinerPrimitiveBoolean  avgt   25   93.846 ± 6.002  ns/op
StringJoinerBenchmarkTest.stringJoinerPrimitiveInt      avgt   25  110.469 ± 2.159  ns/op
StringJoinerBenchmarkTest.stringJoinerPrimitiveLong     avgt   25  109.523 ± 1.101  ns/op

StringJoinerBenchmarkTest.stringJoinerPrimitiveBoolean  thrpt   25  0.011 ±  0.001  ops/ns
StringJoinerBenchmarkTest.stringJoinerPrimitiveInt      thrpt   25  0.009 ±  0.001  ops/ns
StringJoinerBenchmarkTest.stringJoinerPrimitiveLong     thrpt   25  0.009 ±  0.001  ops/ns

After

StringBuilderBenchmarkTest.stringBuilderPrimitiveBoolean  avgt   25  49.887 ± 0.542  ns/op
StringBuilderBenchmarkTest.stringBuilderPrimitiveInt      avgt   25  36.461 ± 0.505  ns/op
StringBuilderBenchmarkTest.stringBuilderPrimitiveLong     avgt   25  37.222 ± 0.157  ns/op

StringBuilderBenchmarkTest.stringBuilderPrimitiveBoolean  thrpt   25  0.020 ±  0.001  ops/ns
StringBuilderBenchmarkTest.stringBuilderPrimitiveInt      thrpt   25  0.026 ±  0.001  ops/ns
StringBuilderBenchmarkTest.stringBuilderPrimitiveLong     thrpt   25  0.026 ±  0.001  ops/ns

Conclusion

I think the StringBuilder version was consistently faster, at least on my JVM 11, Ubuntu Linux. Looking at the code, I thought it would be faster to check the index of the for-loop to avoid adding something that would be later removed, but looks like this could be a follow-up.

+1

Thanks!
Bruno

p.s.: Friday 10 PM, so hopefully I didn't confuse the metrics, I think I got higher throughput with StringBuilder and lower average time with StringBuilder, but feel free to correct me if I read the results incorrectly

p.p.s: Sorry, couldn't test with other OS's or JVM's 👍

return joiner.toString();
return stringBuilder
.deleteCharAt(stringBuilder.length() - 1)
.toString();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, I thought instead of deleteChartAt using an if statement in the for above, to avoid adding the last delimiter would improve more the performance. For a follow-up discussion I think.

@garydgregory
Copy link
Member

@kinow Your results are not that helpful IMO because you're are not measuring useful runs, you've effectively invented new tests and new methodology :( I was able to follow the instructions from @HubertWo without issue, please see his docs.

My impression is that you are measuring so little that there is too much risk for noise: In your tests, you are measuring throughput in hundredths of operations per nanoseconds. In @HubertWo's setup, you measure millions of operations per second. Unless you are running on some LN2-cooled CPU and the rest of us on potatoes, I'm not sure how to account for the difference aside from simply running the tests in a completely different way. As soon as you start editing the sources and changing the JMH annotations, making a simple comparison is hopeless.

Please do take the time to review @HubertWo's docs, it worked after I took the time to follow his readme, nothing special to do. Otherwise, do post what your issue is, I am sure we will all benefit from learning how to deal with JMH better ;)

@kinow
Copy link
Member

kinow commented Nov 19, 2021

@garydgregory will try running the original tests again. I used the test settings of an existing jmh benchmark in lang, though no idea if that's being actually used.

@garydgregory
Copy link
Member

Thank you @kinow ! It will be simpler to compare our results with hardware and OS hopefully the only difference.

@kinow
Copy link
Member

kinow commented Nov 19, 2021

Thank you @kinow ! It will be simpler to compare our results with hardware and OS hopefully the only difference.

I have a Windows 10 here too, but rarely use it. I will try to run the tests on Ubuntu LTS and on Win 10. JVM 8 and 11 only would be enough?

Copy link
Member

@kinow kinow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had given up after a SNAPSHOT couldn't be located, but then just had to mvn install this PR's branch to get the SNAPSHOT installed in my local Maven repo. Here's the results on Ubuntu LTS.

JVM 11

First JVM 11, where I think StringBuilder is the winner with 24_967_220.771 throughput versus StringJoiner's 8_685_054.944.

Apache Maven 3.8.2 (ea98e05a04480131370aa0c110b8c54cf726c06f)
Maven home: /opt/apache-maven-3.8.2
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-90-generic", arch: "amd64", family: "unix"

StringJoiner

Result "com.github.hubertwo.acljmh.StringJoinerBenchmark.stringJoinerPrimitiveInt":
  8685054.944 ±(99.9%) 143639.898 ops/s [Average]
  (min, avg, max) = (8276817.474, 8685054.944, 8968956.809), stdev = 191755.154
  CI (99.9%): [8541415.046, 8828694.843] (assumes normal distribution)


# Run complete. Total time: 00:08:21

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                        Mode  Cnt        Score        Error  Units
StringJoinerBenchmark.stringJoinerPrimitiveInt  thrpt   25  8685054.944 ± 143639.898  ops/s

StringBuilder

Result "com.github.hubertwo.acljmh.StringBuilderBenchmark.stringBuilderPrimitiveInt":
  24967220.771 ±(99.9%) 770683.808 ops/s [Average]
  (min, avg, max) = (21267937.022, 24967220.771, 25980909.331), stdev = 1028840.835
  CI (99.9%): [24196536.963, 25737904.579] (assumes normal distribution)


# Run complete. Total time: 00:08:21

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                          Mode  Cnt         Score        Error  Units
StringBuilderBenchmark.stringBuilderPrimitiveInt  thrpt   25  24967220.771 ± 770683.808  ops/s

JVM 16

Now with JVM 16. I think StringBuilder is a winner here too with 23_751_776.524, while StringJoiner had 8508216.567 throughput.

Apache Maven 3.8.2 (ea98e05a04480131370aa0c110b8c54cf726c06f)
Maven home: /opt/apache-maven-3.8.2
Java version: 16.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-16-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-90-generic", arch: "amd64", family: "unix"

StringJoiner

Result "com.github.hubertwo.acljmh.StringJoinerBenchmark.stringJoinerPrimitiveInt":
  8508216.567 ±(99.9%) 126690.808 ops/s [Average]
  (min, avg, max) = (8046279.518, 8508216.567, 8834546.942), stdev = 169128.605
  CI (99.9%): [8381525.759, 8634907.376] (assumes normal distribution)


# Run complete. Total time: 00:08:21

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                        Mode  Cnt        Score        Error  Units
StringJoinerBenchmark.stringJoinerPrimitiveInt  thrpt   25  8508216.567 ± 126690.808  ops/s

StringBuilder

Result "com.github.hubertwo.acljmh.StringBuilderBenchmark.stringBuilderPrimitiveInt":
  23751776.524 ±(99.9%) 956416.445 ops/s [Average]
  (min, avg, max) = (20958707.696, 23751776.524, 25285000.397), stdev = 1276788.591
  CI (99.9%): [22795360.079, 24708192.969] (assumes normal distribution)


# Run complete. Total time: 00:08:21

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.

Benchmark                                          Mode  Cnt         Score        Error  Units
StringBuilderBenchmark.stringBuilderPrimitiveInt  thrpt   25  23751776.524 ± 956416.445  ops/s

So for now keeping my +1 based on these results for Ubuntu LTS, especially since they were consistent between JVM 11 and JVM 16. Happy to include other JVM's or OS'es in the tests too, if needed.

Bruno

@garydgregory
Copy link
Member

Thank you @kinow ! It will be simpler to compare our results with hardware and OS hopefully the only difference.

I have a Windows 10 here too, but rarely use it. I will try to run the tests on Ubuntu LTS and on Win 10. JVM 8 and 11 only would be enough?

I think so. That is what I used.

@garydgregory
Copy link
Member

Thanks for the update @kinow

This pattern does not make sense to me:

        return stringBuilder
                .deleteCharAt(stringBuilder.length() - 1)
                .toString();

First deleteCharAt is called which calls System.arraycopy, then toString() is called which allocates the new String.

Why not skip the first part by simply saying:

return stringBuilder.substring(0, stringBuilder.length() - 1);

?

Also for the char and boolean versions, it seems like the builder size can be simply computed because each element's String version is a fixed size (assuming 5 for boolean). For ints and longs it might not be worth it as you'd have to assume the max size.

@HubertWo HubertWo force-pushed the fix/LANG-1675_string_join_refactor branch from f2f6cd9 to 2c6dd0e Compare January 15, 2022 11:15
@HubertWo
Copy link
Contributor Author

@garydgregory, @kinow thank you for testing!

Regarding code suggestions. I've changed deleteCharAt to substring and set fixed size of StringBuilder to join(char) and join(boolean).

@garydgregory garydgregory merged commit 0f7d484 into apache:master Apr 3, 2022
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.

6 participants