Feedback for StringTemplate; indify StringTemplate Processor invocation to cache compiled Processor at runtime

ShinyaYoshida bitterfoxc at gmail.com
Thu Mar 28 04:47:32 UTC 2024


Hi,
(Please tell me if there's another proper ML than amber-dev)

I recently played around with StringTemplate.

I developed a StringTemplate Processor for JSON recently:
https://github.com/bitterfox/json-string-template
This parses JSON format strings in fragments with placeholders with
referring values array element.

In the first naive implementation, it tokenizes fragments and parses the
tokens creating Json Objects taking elements from values, and putting the
value as the Json Object key or value.
So this is the one-pass implementation, but it parses fragments every time
the JSON String Template processing is called.
https://github.com/bitterfox/json-string-template/blob/main/json-string-template-core/src/main/java/io/github/bitterfox/json/string/template/core/JsonParserV1.java

In the second implementation, it parses fragments and creates AST for JSON.
After that, it reads the AST again and creates a Lambda expression for
`(Object[] value) -> JSON` that puts an element of values to a part of
JsonObject.
https://github.com/bitterfox/json-string-template/blob/main/json-string-template-core/src/main/java/io/github/bitterfox/json/string/template/core/JsonCompiler.java

Thanks to returning (Object[] value) -> JSON and immutability of fragments
of the string template expression (as long as we use through the string
template expression, it will be the same fragments for the same invocation),
We can now cache the lambda expression and skip parsing fragments to JSON
AST.

This provides us a space for optimization of the performance of JSON String
Template by caching, however, StringTemplate.Processor is called for the
same receiver usually because we usually define StringTemplate.Processor
instance as static final field and use string template expression through
it.
So we need to implement a caching layer by ourselves.
https://github.com/bitterfox/json-string-template/blob/main/json-string-template-core/src/main/java/io/github/bitterfox/json/string/template/core/JsonStringTemplateProcessorV2CachedImpl.java

The downside of such caching implementation is
- The caching layer could be the bottleneck and cause another performance
issue
- We need to depend on the implementation detail of Javac, and JDK for
better performance of caching. For example, I assumed runtime always
creates the same fragments reference
  -
https://github.com/bitterfox/json-string-template/blob/b53898da2d2c70aeee9a36cdffc34ba31460ba28/json-string-template-core/src/main/java/io/github/bitterfox/json/string/template/core/JsonStringTemplateProcessorV2CachedImpl.java#L63
  - This might not work on someday

Now I'm working on a third implementation with Java22 using ClassFileAPI.
Using ClassFileAPI, we can generate a Java class after parsing fragments
that is
```
static JsonObject createJson(Object[] values) {
    return Json.createObjectBuilder().add("name",
Json.createValue(value[0].toString()));
}
```
for JSON"{'name': \{name}}".
Once it's compiled to the Java class and loaded, calling it will have
almost zero overhead even though we run it through string template
expression.

However, as the same reason for the 2nd implementation, there's no caching
mechanism in StringTemplate, still, we need to implement the caching layer
as well as the 2nd approach with the same problems.

Suppose we can couple the compiled class (or (values: Object[]) -> T) to
the invocation of string template processor at string template expression.
In that case, we can solve this issue and provide the best performance for
every StringTemplate Processor in the Java world.

Now we already have a tool for it in JVM, invokedynamic.

What do you think about using invokedynamic not only for creating
StringTemplate objects but also calling StringTemplate Processor invocation
and providing a caching layer at Java spec?

I made a bytecode level PoC for StringTemplate Processor invocation using
invokedynamic with caching for each StringTemplate expression.

https://github.com/bitterfox/indy-string-template-processing/

In the PoC, I changed the interface of StringTemplate Processor.
I added StringTemplateProcessorFactory: (String[] fragments) ->
StringTemplateProcessor.
StringTemplateProcessor is (Object[] values) -> Object (i.e. T).
And it caches StringTemplateProcessor.
StringTemplateProcessorFactory has a method to indicate to cache the
processor or create the processor every time to runtime.

StringTemplateRuntime runs the processing for
StringTemplateProcessorFactory and StringTemplate(fragments and values).
If cache is required, it creates a processor only once.

StringTemplateRuntime is used for the MethodHandle coupled to the
invokedynamic.
StringTemplateBSM#createStringTemplateRuntimeCallSite is a BSM for the
invokedynamic.

If we compile
doStringTemplate() {
    Sting name = "duke";
    System.out.println(MY_STP."Hello \{name}");
}

It will generate classfile like

  public static void doStringTemplate();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=6, locals=2, args_size=0
         0: getstatic     #10                 // Field
StringTemplateSTRDebug.STR:LStringTemplateProcessorFactory;
         3: iconst_2
         4: anewarray     #12                 // class java/lang/String
         7: dup
         8: iconst_0
         9: ldc           #14                 // String Hello
        11: aastore
        12: dup
        13: iconst_1
        14: ldc           #16                 // String
        16: aastore
        17: iconst_1
        18: anewarray     #18                 // class java/lang/Object
        21: dup
        22: iconst_0
        23: ldc           #20                 // String duke
        25: aastore
        26: invokedynamic #31,  0             // InvokeDynamic
#0:process:(LStringTemplateProcessorFactory;[Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;
        31: astore_1
        32: getstatic     #37                 // Field
java/lang/System.out:Ljava/io/PrintStream;
        35: aload_1
        36: invokevirtual #43                 // Method
java/io/PrintStream.println:(Ljava/lang/Object;)V
        39: return

BootstrapMethods:
  0: #27 REF_invokeStatic
StringTemplateBSM.createStringTemplateRuntimeCallSite:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:

This creates string template processor only once for this method
```
// call doStringTemplate
Create new processor, some processor may parse fragments, so slow
Process string template
Hello duke
// call doStringTemplate
Process string template
Hello duke
// call doStringTemplate
Process string template
Hello duke
```


This kind of mechanism will be useful for most of string template
processors like
- FMT: This parses formatter string like %02d
- SQL: Same overhead with queryBuilder.select("*").from(...).where(...) is
preferred
- LocalizationProcessor: should avoid loading resource bundle every time
- ...

Let me know your thoughts about this approach for StringTemplate Processor
invocation

Best regards,
Shinya Yoshida (@bitter_fox, @shinyafox)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20240328/7905b65b/attachment-0001.htm>


More information about the amber-dev mailing list