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


Reply via email to