Re: RFR: 8333396: Performance regression of DecimalFormat.format [v12]

2024-06-27 Thread Justin Lu
On Thu, 27 Jun 2024 11:19:44 GMT, lingjun-cg  wrote:

>> src/java.base/share/classes/java/text/StringBufFactory.java line 45:
>> 
>>> 43: }
>>> 44: 
>>> 45: private static class StringBufferImpl implements Format.StringBuf {
>> 
>> The implementations may be more concise as a `record class`
>
> I know little about `record class`, it seems `record class` is help to model 
> data aggregation, but here it act as a proxy class.

Yes, but since these implementations are just proxy classes, I think it is even 
more the reason to make them as simple/concise as possible since we don't care 
too much about what they do. (Since for most part it's the same as normal 
StringBuf/Bldr)

Either is fine here, we can stick with as is if you prefer.

-

PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1657646453


Re: RFR: 8333396: Performance regression of DecimalFormat.format [v12]

2024-06-27 Thread lingjun-cg
On Thu, 27 Jun 2024 05:08:06 GMT, Justin Lu  wrote:

>> lingjun-cg has updated the pull request incrementally with one additional 
>> commit since the last revision:
>> 
>>   896: Performance regression of DecimalFormat.format
>
> src/java.base/share/classes/java/text/StringBufFactory.java line 45:
> 
>> 43: }
>> 44: 
>> 45: private static class StringBufferImpl implements Format.StringBuf {
> 
> The implementations may be more concise as a `record class`

I know little about `record class`, it seems `record class` is help to model 
data aggregation, but here it act as a proxy class.

-

PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1656957129


Re: RFR: 8333396: Performance regression of DecimalFormat.format [v12]

2024-06-26 Thread Justin Lu
On Wed, 26 Jun 2024 09:43:32 GMT, lingjun-cg  wrote:

>> ### Performance regression of DecimalFormat.format
>> From the output of perf, we can see the hottest regions contain atomic 
>> instructions.  But when run with JDK 11, there is no such problem. The 
>> reason is the removed biased locking.  
>> The DecimalFormat uses StringBuffer everywhere, and StringBuffer itself 
>> contains many synchronized methods.
>> So I added support for some new methods that accept StringBuilder which is 
>> lock-free.
>> 
>> ### Benchmark testcase
>> 
>> @BenchmarkMode(Mode.AverageTime)
>> @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
>> @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
>> @State(Scope.Thread)
>> @OutputTimeUnit(TimeUnit.NANOSECONDS)
>> public class JmhDecimalFormat {
>> 
>> private DecimalFormat format;
>> 
>> @Setup(Level.Trial)
>> public void setup() {
>> format = new DecimalFormat("#0.0");
>> }
>> 
>> @Benchmark
>> public void testNewAndFormat() throws InterruptedException {
>> new DecimalFormat("#0.0").format(9524234.1236457);
>> }
>> 
>> @Benchmark
>> public void testNewOnly() throws InterruptedException {
>> new DecimalFormat("#0.0");
>> }
>> 
>> @Benchmark
>> public void testFormatOnly() throws InterruptedException {
>> format.format(9524234.1236457);
>> }
>> }
>> 
>> 
>> ### Test result
>>  Current JDK before optimize
>> 
>>  Benchmark Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnly   avgt   50  642.099 ? 1.253  ns/op
>> JmhDecimalFormat.testNewAndFormat avgt   50  989.307 ? 3.676  ns/op
>> JmhDecimalFormat.testNewOnly  avgt   50  303.381 ? 5.252  ns/op
>> 
>> 
>> 
>>  Current JDK after optimize
>> 
>> Benchmark  Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnlyavgt   50  351.499 ? 0.761  ns/op
>> JmhDecimalFormat.testNewAndFormat  avgt   50  615.145 ? 2.478  ns/op
>> JmhDecimalFormat.testNewOnly   avgt   50  209.874 ? 9.951  ns/op
>> 
>> 
>> ### JDK 11 
>> 
>> Benchmark  Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnlyavgt   50  364.214 ? 1.191  ns/op
>> JmhDecimalFormat.testNewAndFormat  avgt   50  658.699 ? 2.311  ns/op
>> JmhDecimalFormat.testNewOnly   avgt   50  248.300 ? 5.158  ns/op
>
> lingjun-cg has updated the pull request incrementally with one additional 
> commit since the last revision:
> 
>   896: Performance regression of DecimalFormat.format

Thanks for all the updates. Looks good to me pending the other's comments.

(BTW, the benchmark can go to -> _test/micro/org/openjdk/bench/java/text/_ as 
opposed to _test/jdk/java/text_)

src/java.base/share/classes/java/text/CompactNumberFormat.java line 569:

> 567: @Override
> 568: StringBuilder format(Object number,
> 569:  StringBuilder toAppendTo,

nit: indentation seems odd here/not consistent with the other corresponding 
method.

src/java.base/share/classes/java/text/SimpleDateFormat.java line 1330:

> 1328: 
> 1329: int num = (value / 60) * 100 + (value % 60);
> 1330: if (buffer.isProxyStringBuilder()) {

If we made the implementations of `StringBuf` records, we could use record 
patterns and do away with the `isProxyStringBuilder()` method. Although we 
would have to change the visibility of the implementations, so maybe not...

src/java.base/share/classes/java/text/StringBufFactory.java line 29:

> 27: 
> 28: /**
> 29:  * StringBufFactory create {code Format.StringBuf}'s implements that

Nit: Just to improve some grammar, how about something like,


 * {@code StringBufFactory} creates implementations of {@code Format.StringBuf},
 * which is an interface with the minimum overlap required to support {@code 
StringBuffer}
 * and {@code StringBuilder} in {@code Format}. This allows for {@code 
StringBuilder} to be used
 * in place of {@code StringBuffer} to provide performance benefits for JDK 
internal 
 * {@code Format} subclasses.

src/java.base/share/classes/java/text/StringBufFactory.java line 37:

> 35: final class StringBufFactory {
> 36: 
> 37: static Format.StringBuf of(StringBuffer sb) {

At least for the factory class, let's just `import 
java.text.Format.StringBuf;`, so we don't have to use the full name everywhere.

src/java.base/share/classes/java/text/StringBufFactory.java line 45:

> 43: }
> 44: 
> 45: private static class StringBufferImpl implements Format.StringBuf {

The implementations may be more concise as a `record class`

-

PR Review: https://git.openjdk.org/jdk/pull/19513#pullrequestreview-2144190566
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1656424171
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1656446980
PR Review Comment: 

Re: RFR: 8333396: Performance regression of DecimalFormat.format [v12]

2024-06-26 Thread Chen Liang
On Wed, 26 Jun 2024 09:43:32 GMT, lingjun-cg  wrote:

>> ### Performance regression of DecimalFormat.format
>> From the output of perf, we can see the hottest regions contain atomic 
>> instructions.  But when run with JDK 11, there is no such problem. The 
>> reason is the removed biased locking.  
>> The DecimalFormat uses StringBuffer everywhere, and StringBuffer itself 
>> contains many synchronized methods.
>> So I added support for some new methods that accept StringBuilder which is 
>> lock-free.
>> 
>> ### Benchmark testcase
>> 
>> @BenchmarkMode(Mode.AverageTime)
>> @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
>> @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
>> @State(Scope.Thread)
>> @OutputTimeUnit(TimeUnit.NANOSECONDS)
>> public class JmhDecimalFormat {
>> 
>> private DecimalFormat format;
>> 
>> @Setup(Level.Trial)
>> public void setup() {
>> format = new DecimalFormat("#0.0");
>> }
>> 
>> @Benchmark
>> public void testNewAndFormat() throws InterruptedException {
>> new DecimalFormat("#0.0").format(9524234.1236457);
>> }
>> 
>> @Benchmark
>> public void testNewOnly() throws InterruptedException {
>> new DecimalFormat("#0.0");
>> }
>> 
>> @Benchmark
>> public void testFormatOnly() throws InterruptedException {
>> format.format(9524234.1236457);
>> }
>> }
>> 
>> 
>> ### Test result
>>  Current JDK before optimize
>> 
>>  Benchmark Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnly   avgt   50  642.099 ? 1.253  ns/op
>> JmhDecimalFormat.testNewAndFormat avgt   50  989.307 ? 3.676  ns/op
>> JmhDecimalFormat.testNewOnly  avgt   50  303.381 ? 5.252  ns/op
>> 
>> 
>> 
>>  Current JDK after optimize
>> 
>> Benchmark  Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnlyavgt   50  351.499 ? 0.761  ns/op
>> JmhDecimalFormat.testNewAndFormat  avgt   50  615.145 ? 2.478  ns/op
>> JmhDecimalFormat.testNewOnly   avgt   50  209.874 ? 9.951  ns/op
>> 
>> 
>> ### JDK 11 
>> 
>> Benchmark  Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnlyavgt   50  364.214 ? 1.191  ns/op
>> JmhDecimalFormat.testNewAndFormat  avgt   50  658.699 ? 2.311  ns/op
>> JmhDecimalFormat.testNewOnly   avgt   50  248.300 ? 5.158  ns/op
>
> lingjun-cg has updated the pull request incrementally with one additional 
> commit since the last revision:
> 
>   896: Performance regression of DecimalFormat.format

src/java.base/share/classes/java/text/Format.java line 193:

> 191: 
> 192: StringBuilder format(Object obj,
> 193:  StringBuilder toAppendTo,

Can we change this type to `StringBuf`, like `StringBuf format(Object obj, 
StringBuf toAppendTo, FieldPosition pos)`? Then we can remove a few 
`StringBuilder` overloads by just using the `StringBuf` version.

src/java.base/share/classes/java/text/NumberFormat.java line 424:

> 422: 
> 423: StringBuilder format(double number,
> 424:  StringBuilder toAppendTo,

Same remark, `StringBuf format(double number, StringBuf toAppendTo, 
FieldPosition pos)`

src/java.base/share/classes/java/text/SimpleDateFormat.java line 1034:

> 1032: @Override
> 1033: public AttributedCharacterIterator formatToCharacterIterator(Object 
> obj) {
> 1034: StringBuilder sb = new StringBuilder();

On second thought, we can just make this `StringBuf sb = 
StringBufFactory.of(new StringBuilder())` - or just add `StringBufFactory.of()` 
which defaults to create a StringBuilder-backed buf.

src/java.base/share/classes/java/text/SimpleDateFormat.java line 1448:

> 1446: numberFormat.setMaximumIntegerDigits(maxDigits);
> 1447: if (buffer.isProxyStringBuilder()) {
> 1448: numberFormat.format((long)value, buffer.asStringBuilder(), 
> DontCareFieldPosition.INSTANCE);

This `numberFormat` might be a user class; if that's the case, we might do 
something like:

if (buffer.isProxyStringBuilder()) {
var nf = numberFormat; // field is protected, users can change it even with 
races!
if (nf.isInternalSubclass()) {
nf.format((long)value, buffer.asStringBuilder(), 
DontCareFieldPosition.INSTANCE);
} else {
// format to stringbuffer, and add that buffer to the builder
buffer.append(nf.format((long)value, new StringBuffer(), 
DontCareFieldPosition.INSTANCE));
}
} else { // existing code

-

PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655595282
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655596623
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655605616
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655603917


Re: RFR: 8333396: Performance regression of DecimalFormat.format [v12]

2024-06-26 Thread Naoto Sato
On Wed, 26 Jun 2024 09:43:32 GMT, lingjun-cg  wrote:

>> ### Performance regression of DecimalFormat.format
>> From the output of perf, we can see the hottest regions contain atomic 
>> instructions.  But when run with JDK 11, there is no such problem. The 
>> reason is the removed biased locking.  
>> The DecimalFormat uses StringBuffer everywhere, and StringBuffer itself 
>> contains many synchronized methods.
>> So I added support for some new methods that accept StringBuilder which is 
>> lock-free.
>> 
>> ### Benchmark testcase
>> 
>> @BenchmarkMode(Mode.AverageTime)
>> @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
>> @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
>> @State(Scope.Thread)
>> @OutputTimeUnit(TimeUnit.NANOSECONDS)
>> public class JmhDecimalFormat {
>> 
>> private DecimalFormat format;
>> 
>> @Setup(Level.Trial)
>> public void setup() {
>> format = new DecimalFormat("#0.0");
>> }
>> 
>> @Benchmark
>> public void testNewAndFormat() throws InterruptedException {
>> new DecimalFormat("#0.0").format(9524234.1236457);
>> }
>> 
>> @Benchmark
>> public void testNewOnly() throws InterruptedException {
>> new DecimalFormat("#0.0");
>> }
>> 
>> @Benchmark
>> public void testFormatOnly() throws InterruptedException {
>> format.format(9524234.1236457);
>> }
>> }
>> 
>> 
>> ### Test result
>>  Current JDK before optimize
>> 
>>  Benchmark Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnly   avgt   50  642.099 ? 1.253  ns/op
>> JmhDecimalFormat.testNewAndFormat avgt   50  989.307 ? 3.676  ns/op
>> JmhDecimalFormat.testNewOnly  avgt   50  303.381 ? 5.252  ns/op
>> 
>> 
>> 
>>  Current JDK after optimize
>> 
>> Benchmark  Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnlyavgt   50  351.499 ? 0.761  ns/op
>> JmhDecimalFormat.testNewAndFormat  avgt   50  615.145 ? 2.478  ns/op
>> JmhDecimalFormat.testNewOnly   avgt   50  209.874 ? 9.951  ns/op
>> 
>> 
>> ### JDK 11 
>> 
>> Benchmark  Mode  CntScore   Error  Units
>> JmhDecimalFormat.testFormatOnlyavgt   50  364.214 ? 1.191  ns/op
>> JmhDecimalFormat.testNewAndFormat  avgt   50  658.699 ? 2.311  ns/op
>> JmhDecimalFormat.testNewOnly   avgt   50  248.300 ? 5.158  ns/op
>
> lingjun-cg has updated the pull request incrementally with one additional 
> commit since the last revision:
> 
>   896: Performance regression of DecimalFormat.format

Thanks for the changes. Since this is a performance improvement, I also would 
like to have the microbenchmark that you used as the test case.

src/java.base/share/classes/java/text/Format.java line 427:

> 425: /**
> 426:  * Used to distinguish JDK internal subclass and user-defined 
> subclass
> 427:  * of {code Format}.

There are a lot of places which is missing `@` for `code` tag in the comments.

src/java.base/share/classes/java/text/Format.java line 434:

> 432: boolean isInternalSubclass() {
> 433: return false;
> 434: }

Do we need this? Should comparing the package name with 'java.text' be 
sufficient?

src/java.base/share/classes/java/text/Format.java line 438:

> 436: /**
> 437:  * StringBuf is the minimal common interface of {code StringBuffer} 
> and {code StringBuilder}.
> 438:  * It used by the various {code Format} implementations as the 
> internal string buffer.

typo: "is" is missing

src/java.base/share/classes/java/text/Format.java line 440:

> 438:  * It used by the various {code Format} implementations as the 
> internal string buffer.
> 439:  */
> 440: interface StringBuf {

Can we make it `sealed`?

src/java.base/share/classes/java/text/ListFormat.java line 365:

> 363: return format(input, new StringBuffer(),
> 364: DontCareFieldPosition.INSTANCE).toString();
> 365: }

Since `ListFormat` is a final class, no need to have a condition, always use 
StringBuilder version.

src/java.base/share/classes/java/text/StringBufFactory.java line 35:

> 33:  * a better performance.
> 34:  */
> 35: final class StringBufFactory {

Add a private no-arg constructor so that no instance can be created for 
`StringBufFactory` itself.

-

PR Review: https://git.openjdk.org/jdk/pull/19513#pullrequestreview-2143027077
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655560390
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655563089
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655565976
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r169020
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655566915
PR Review Comment: https://git.openjdk.org/jdk/pull/19513#discussion_r1655571771


Re: RFR: 8333396: Performance regression of DecimalFormat.format [v12]

2024-06-26 Thread lingjun-cg
> ### Performance regression of DecimalFormat.format
> From the output of perf, we can see the hottest regions contain atomic 
> instructions.  But when run with JDK 11, there is no such problem. The reason 
> is the removed biased locking.  
> The DecimalFormat uses StringBuffer everywhere, and StringBuffer itself 
> contains many synchronized methods.
> So I added support for some new methods that accept StringBuilder which is 
> lock-free.
> 
> ### Benchmark testcase
> 
> @BenchmarkMode(Mode.AverageTime)
> @Warmup(iterations = 5, time = 500, timeUnit = TimeUnit.MILLISECONDS)
> @Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
> @State(Scope.Thread)
> @OutputTimeUnit(TimeUnit.NANOSECONDS)
> public class JmhDecimalFormat {
> 
> private DecimalFormat format;
> 
> @Setup(Level.Trial)
> public void setup() {
> format = new DecimalFormat("#0.0");
> }
> 
> @Benchmark
> public void testNewAndFormat() throws InterruptedException {
> new DecimalFormat("#0.0").format(9524234.1236457);
> }
> 
> @Benchmark
> public void testNewOnly() throws InterruptedException {
> new DecimalFormat("#0.0");
> }
> 
> @Benchmark
> public void testFormatOnly() throws InterruptedException {
> format.format(9524234.1236457);
> }
> }
> 
> 
> ### Test result
>  Current JDK before optimize
> 
>  Benchmark Mode  CntScore   Error  Units
> JmhDecimalFormat.testFormatOnly   avgt   50  642.099 ? 1.253  ns/op
> JmhDecimalFormat.testNewAndFormat avgt   50  989.307 ? 3.676  ns/op
> JmhDecimalFormat.testNewOnly  avgt   50  303.381 ? 5.252  ns/op
> 
> 
> 
>  Current JDK after optimize
> 
> Benchmark  Mode  CntScore   Error  Units
> JmhDecimalFormat.testFormatOnlyavgt   50  351.499 ? 0.761  ns/op
> JmhDecimalFormat.testNewAndFormat  avgt   50  615.145 ? 2.478  ns/op
> JmhDecimalFormat.testNewOnly   avgt   50  209.874 ? 9.951  ns/op
> 
> 
> ### JDK 11 
> 
> Benchmark  Mode  CntScore   Error  Units
> JmhDecimalFormat.testFormatOnlyavgt   50  364.214 ? 1.191  ns/op
> JmhDecimalFormat.testNewAndFormat  avgt   50  658.699 ? 2.311  ns/op
> JmhDecimalFormat.testNewOnly   avgt   50  248.300 ? 5.158  ns/op

lingjun-cg has updated the pull request incrementally with one additional 
commit since the last revision:

  896: Performance regression of DecimalFormat.format

-

Changes:
  - all: https://git.openjdk.org/jdk/pull/19513/files
  - new: https://git.openjdk.org/jdk/pull/19513/files/b6646c5d..7eb9b523

Webrevs:
 - full: https://webrevs.openjdk.org/?repo=jdk=19513=11
 - incr: https://webrevs.openjdk.org/?repo=jdk=19513=10-11

  Stats: 242 lines in 9 files changed: 220 ins; 0 del; 22 mod
  Patch: https://git.openjdk.org/jdk/pull/19513.diff
  Fetch: git fetch https://git.openjdk.org/jdk.git pull/19513/head:pull/19513

PR: https://git.openjdk.org/jdk/pull/19513