On 04.10.2016 18:00, Graeme Rocher wrote:
[...] If you combine this
will my other proposal around allowing delegates to on maps you can
see that you could implement markup builder for static compilation
mkp.html {
body {
div id:"test"
}
}
ok, let us assume mkp is a @SDyn style object/builder and html, body, as
well as div are actually realized by methodMissing... Where will you
then put the @DelegatesTo with your Map backed by a static class, that
tells us, that id is a valid map key for this invocation?
What you gain is performance, statically compiled builders will be
much faster because Groovy throws exceptions during method dispatch
(to resolve closure properties). Statically compiled code will give
you direct method dispatch to the method, whilst dynamic code will go
directly to invokeMethod.
But if you want maximum performance you would need to have each element
backed by a method.
We see large performance gains when using
statically compiled JSON views due to:
https://github.com/grails/grails-views/blob/master/core/src/main/groovy/grails/views/compiler/BuilderTypeCheckingExtension.groovy
ok, let us put some numbers behind this... Let us look at 3 variants for
the builders. One is the traditional Groovy style with delegate and
invokeMethod/methodMissing, the next variant is using mkp.body and
mkp.div, so a lookup-path using the delegate is not taken. And then of
course the variant where we actually do not go to methodMissing, but to
real methods.
so I made myself a small unprofessional program to measure:
10.times {
def start = System.nanoTime()
1_000_000.times {
foo()
}
def end = System.nanoTime()
println "time : ${(end-start).intdiv(1_000_000)}ms"
}
to take the most stable value of 10 iterations of 1 million calls. Of
course only for a small builder. So let us first look at the static
cases... what @CompileStatic with @DelegatesTo does is actually not
depend on the delegate of the Closure, but to call the methods directly
there... more or less. You actually have to get the delegate first, then
make the call on the delegate... but we will skip this at first. Also I
have to mention, that the measurement itself costs too, but I ignore
this here totally.
@CompileStatic
class Builder {
def methodMissing(String name, args) {
def realArgs = (Object[]) args
def last = realArgs[-1]
if (last instanceof Closure) last.call(name)
}
}
@CompileStatic
def foo() {
def b = new Builder()
b.methodMissing("html") {
b.methodMissing("body") {
b.methodMissing("div", [id:"aDiv"])
}
}
}
This is about what I think @CompileStatic + @SDyn would produce. Time:
~690 ms
@CompileStatic
class Builder {
def html(@DelegatesTo(Builder) Closure c) {
c.call()
}
def body(@DelegatesTo(Builder) Closure c) {
c.call()
}
def div(Map m) {
}
}
@CompileStatic
def foo() {
def b = new Builder()
b.html {
b.body {
b.div ([id:"aDiv"])
}
}
}
This would be without backing by methodMissing. Time: ~280 ms
As you can see, even though we do here direct method calls in both
cases, do not depend on the MOP... considering that call() will still
have a partially dynamic call to doCall() here, it could make you wonder
where these 400ms are going... I was thinking of a megamorphic callsite
for last.call(name), but I am not sure.
class Builder {
def methodMissing(String name, args) {
def last = args[-1]
if (last instanceof Closure) {
last.delegate = this
last(name)
}
}
}
def foo() {
def b = new Builder()
b.html() {
body() {
div (id:"aDiv")
}
}
}
This is more the classic builder style. Time: ~14600ms
Almost 15s is of course quite the number, about factor 20 to the slower
static variant. But this can actually be improved:
class Builder {
def html(c) {
c.delegate = this
c()
}
def body(c) {
c.delegate = this
c()
}
def div(Map m) {
}
}
def foo() {
def b = new Builder()
b.html() {
body() {
div (id:"aDiv")
}
}
}
Time: ~870ms
As you can see, just not going through methodMissing can be a huge
improvement. Of course this is still like factor 4 compared to the fast
static variant. But maybe what happens if we also do the same technique
to not to depend on the delegate?
class Builder {
def methodMissing(String name, args) {
def last = args[-1]
if (last instanceof Closure) last(name)
}
}
def foo() {
def b = new Builder()
b.html() {
b.body() {
b.div (id:"aDiv")
}
}
}
Time: ~870ms
back to the methodMissing variant and no gain in performance, even
though we excluded the delegate. But this also shows, the MOP doesn´t
have to be slow. In my slow example I was actually not setting the
delegation strategy:
class Builder {
def methodMissing(String name, args) {
def last = args[-1]
if (last instanceof Closure) {
last.delegate = this
last.resolveStrategy = Closure.DELEGATE_ONLY
last(name)
}
}
}
def foo() {
def b = new Builder()
b.html() {
body() {
div (id:"aDiv")
}
}
}
Time: 1500ms
Suddenly we are factor 10 faster then before. This is because the
default strategy will cause the meta class to first search through all
owners and their parents before looking at the delegate. Depending on
the nesting level, this can be huge. With this our dynamic standard
builder is only at about factor 2 compared to your suggestion. And of
course factor 6 to the faster one.
Adopting the strategy with backing the builder by real methods we could
actually gain a bit performance:
class Builder {
def html(c) {
c()
}
def body(c) {
c()
}
def div(Map m) {
}
}
def foo() {
def b = new Builder()
b.html() {
b.body() {
b.div (id:"aDiv")
}
}
}
Time: ~870ms
For this version the resolving strategy actually plays no role, because
we "statically" resolved that already. But I was not actually true to
what would be done, was I:
class Builder {
def html(c) {
c.delegate = this
c.resolveStrategy = Closure.DELEGATE_ONLY
c()
}
def body(c) {
c.delegate = this
c.resolveStrategy = Closure.DELEGATE_ONLY
c()
}
def div(Map m) {
}
}
def foo() {
def b = new Builder()
b.html() {
delegate.body() {
delegate.div (id:"aDiv")
}
}
}
Time: ~570ms
This is actually a bit surprising for me and I have no explanation why
this version is so much faster. Anyway...
lass Builder {
def html(c) {
c()
}
def body(c) {
c()
}
def div(Map m) {
}
}
def foo() {
def b = new Builder()
b.html() {
b.body() {
b.div (id:"aDiv")
}
}
}
Time: ~500ms, depending on delegate: ~540ms
This turns out being the fastest dynamic variant.
Ok, one last variant... this mail is probably confusing everyone out
there already:
class Builder {
def methodMissing(String name, args) {
def last = args[-1]
if (last instanceof Closure) {
last.delegate = this
last(name)
}
}
}
def foo() {
def b = new Builder()
b.html() {
delegate.body() {
delegate.div (id:"aDiv")
}
}
}
Time ~960ms
so fastest possible methodMissing variant is only factor 1.5 to the
static methodMissing variant now.
So we have to think about our goal here. Do we want a fully dynamic but
faster builder, then we should think how to get to the last variant in
this mail here. Do you want to squeeze out whatever is possible? Then we
need to talk about replacing Closure as well actually.
bye Jochen