Skip to content

Conversation

@winfriedgerlach
Copy link

assuming the minimum Java version 11 is correct (as mentioned in the 4.0.3 release notes), this should be a viable performance improvement for the very common case of processing a String event from the SAX parser

Copy link
Contributor

@laurentschoelens laurentschoelens left a comment

Choose a reason for hiding this comment

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

Maybe simplify code for limiting instanceof ?

Comment on lines +108 to +114
else if (pcdata instanceof StringBuilder) {
((StringBuilder) pcdata).getChars(0, len, buf, 0);
}
else if (pcdata instanceof StringBuffer) {
((StringBuffer) pcdata).getChars(0, len, buf, 0);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you use instanceof AbstractStringBuilder since it's extented by both StringBuilder and StringBuffer ?

Copy link
Author

Choose a reason for hiding this comment

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

would like to but that class is not public...

Copy link
Contributor

Choose a reason for hiding this comment

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

oups, yes you're right :)

Copy link

Choose a reason for hiding this comment

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

CharSequence is what you need, implemented by String, StringBuilder, StringBuffer...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes but

  • it's already method argument and type of pcdata
  • it doesn't bring optimized method getChars

Copy link

Choose a reason for hiding this comment

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

Ah, sorry. getChars is there since JDK 25 only. I did miss that.

@laurentschoelens
Copy link
Contributor

@winfriedgerlach : you should also sign eclipsefnd/eca for this to be merged by owners of jaxb-ri repository

@winfriedgerlach winfriedgerlach changed the title unmarshaller performance: use bulk getChars() where possible unmarshaller performance: use bulk operations instead of charAt() where possible Nov 7, 2025
((StringBuffer) pcdata).getChars(0, len, buf, 0);
}
else if (pcdata instanceof Pcdata) {
((Pcdata) pcdata).writeTo(buf, 0);
Copy link
Author

@winfriedgerlach winfriedgerlach Nov 7, 2025

Choose a reason for hiding this comment

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

looking at the implementation, this should give massive gains for Base64Data, but IntArrayData and IntData also benefit

Copy link
Contributor

@laurentschoelens laurentschoelens left a comment

Choose a reason for hiding this comment

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

LGTM

@winfriedgerlach
Copy link
Author

@winfriedgerlach : you should also sign eclipsefnd/eca for this to be merged by owners of jaxb-ri repository

@laurentschoelens done

@laurentschoelens
Copy link
Contributor

That would be interesting to have benchmark on this, even a small test case, to see profiling results on actual 4.0.6 and patched one

@winfriedgerlach winfriedgerlach force-pushed the improve-perfomance-of-unmarshaller branch from 47e3079 to caf8ed2 Compare November 9, 2025 08:59
@winfriedgerlach
Copy link
Author

That would be interesting to have benchmark on this, even a small test case, to see profiling results on actual 4.0.6 and patched one

@laurentschoelens you are completely right, the only problem was that changing the code takes five minutes and benchmarking done right takes hours... So I benchmarked quite a lot at the weekend at it turns out that this change does not move the needle at all in the grand scheme of things for our use-case (parsing lots of XML files with SAX and XML Schema validation). I tried Java 21, 25, and 17, and the results were all within the margin of error.

I didn't test the Base64 use-case though.

I am fine if you close this PR as "premature optimization". I can also investigate further if this helps anyone.

@laurentschoelens
Copy link
Contributor

No I think that should be better optimized code than actual one. What did you look at ? Run time ? Cpu ? Memory ?

@winfriedgerlach
Copy link
Author

winfriedgerlach commented Nov 10, 2025

No I think that should be better optimized code than actual one. What did you look at ? Run time ? Cpu ? Memory ?

I wrote a JMH test that parsed ~5,000 XML documents from memory and looked at the "operations per second" metric. I tried with both JDK default and Woodstox parser (the latter is ~2x faster...). I fully agree that the code change proposed in this PR will make things faster, the question is only whether performance in this area is relevant in the overall parse-and-validate process.

I can do some additional benchmark runs tonight where I only look at the changed code section in isolation.

@laurentschoelens
Copy link
Contributor

Could you share the benchmark code you made ?

@winfriedgerlach
Copy link
Author

winfriedgerlach commented Nov 10, 2025

@laurentschoelens OK, here are some micro benchmarks that really only test the for ... charAt() vs. getChars()/writeTo() part. All tests have been performed on a Ryzen 7700 desktop under Windows 11 and temurin 21.0.5+11-LTS.

100_000 Strings of length 10: speedup ~1.85
Benchmark                 Mode  Cnt     Score     Error  Units
Isolated.stringCharAt    thrpt   15  1108,888 ±  51,021  ops/s
Isolated.stringGetChars  thrpt   15  2063,215 ± 116,811  ops/s

100_000 String*Builders* of length 10: speedup ~1.7
Benchmark                        Mode  Cnt     Score    Error  Units
IsolatedStringBuilder.charAt    thrpt   15  1304,567 ± 56,170  ops/s
IsolatedStringBuilder.getChars  thrpt   15  2231,755 ± 67,601  ops/s

100_000 IntData of random 0..999_999: speedup ~4.5
Benchmark                         Mode  Cnt     Score    Error  Units
IsolatedIntData.intDataCharAt    thrpt   15   223,689 ±  0,836  ops/s
IsolatedIntData.intDataGetChars  thrpt   15  1020,029 ± 21,783  ops/s

1 IntArrayData with 10 ints of random 0..999_999: speedup ~1.1
Benchmark                              Mode  Cnt         Score         Error  Units
IsolatedIntArrayData.intArrayCharAt   thrpt   15  85888294,441 ± 2359727,336  ops/s
IsolatedIntArrayData.intArrayWriteTo  thrpt   15  93751403,476 ±  772306,405  ops/s
100_000 IntArrayData with each containing 10 random ints in the range of 0..999_999: speedup ~11
IsolatedIntArrayData.intArrayCharAt   thrpt   15   49,950 ±  0,590  ops/s
IsolatedIntArrayData.intArrayWriteTo  thrpt   15  537,718 ± 14,146  ops/s

100_000 Base64Data of 1000 bytes: speedup ~2.3
Benchmark                          Mode  Cnt   Score   Error  Units
IsolatedBase64Data.base64CharAt   thrpt   15   5,714 ± 0,312  ops/s
IsolatedBase64Data.base64WriteTo  thrpt   15  13,032 ± 0,223  ops/s
about the same result when going to 10_000 bytes length:
IsolatedBase64Data.base64CharAt   thrpt   15  0,590 ± 0,023  ops/s
IsolatedBase64Data.base64WriteTo  thrpt   15  1,366 ± 0,019  ops/s

Things to note:

  • IntData is so bad, because it doesn't cache its toString() calls (as opposed to IntArrayData.getLiteral())
  • I have serious doubts whether IntData is used at all due to blatant bugs in its sizeTable --> I was only able to conduct the benchmark with a patched version that calculates stringSizeOfInt(54321) = 5 instead of 6...
  • I do not really have an explanation why IntArrayData performs so badly. I couldn't find a reason in the source code that would explain such a massive speedup --> maybe I made a mistake? But again, is IntArrayData even really used anywhere?

IsolatedString.java
IsolatedStringBuilder.java
IsolatedIntData.java
IntData.java
IsolatedIntArrayData.java
IsolatedBase64Data.java

@laurentschoelens
Copy link
Contributor

About IntArrayData, from what I see

  • in extends PcData, calling writeTo(char[] buf, int start) will do the default impl of the method
    public void writeTo(char[] buf, int start) {
        toString().getChars(0,length(),buf,start);
    }
  • the toString() method in IntArrayData calls the following
@Override
    public String toString() {
        return literal.toString();
    }
  • sadly, if none of the length, charAt or subSequence is called before the toString, the literal var will be null.

Maybe you pointed out a bug but I guess the toString method should call getLiteral method instead of potential null variable.

@laurentschoelens
Copy link
Contributor

I'll try to look further at the rest of your code but that's good news for performance overhead (us are still us gained 😄)

@winfriedgerlach
Copy link
Author

winfriedgerlach commented Nov 11, 2025

@laurentschoelens good finding, I would definitely prefer getLiteral() there instead of direct field access!
But I'm not sure whether this fully explains the speedup, because I do call .length() on each IntArrayData in the setup() method of my JMH benchmark... (cf. IsolatedIntArrayData.java)

Note that there are no usages of IntArrayData in this repo, so we probably shouldn't put much effort in here...

@laurentschoelens
Copy link
Contributor

If you didn't call the length method, it would definitively fail with NPE

I guess having charAt method calling getLiteral on each character and doing checkIndex would slow down this as much as observe :

    @Override
    public char charAt(int index) {
        return getLiteral().charAt(index);
    }
    @Override
    public char charAt(int index) {
        checkIndex(index, count);
        if (isLatin1()) {
            return (char)(value[index] & 0xff);
        }
        return StringUTF16.charAt(value, index);
    }

@winfriedgerlach
Copy link
Author

winfriedgerlach commented Nov 14, 2025

I found this in the Woodstox source code regarding the same matter:
image
nice optimization!

@laurentschoelens
Copy link
Contributor

I found this in the Woodstox source code regarding the same matter:
image
nice optimization!

I which context this is done ?
I mean, is it better to do charAt at some points and getChars for other here too ?

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.

3 participants