Track bound dynamic variables to support binding in async mechanisms.

Description

The issue has been raised before:

While the reasoning behind the proposal is still valid, the original approach has made no progress due to the performance penalty. I have implemented a simplified approach with mutable JavaScript datastructures to minimize the performance impact. Because we are single-threaded we can use js assignment and don't need to port Clojure's binding frame. A small penalty is paid by the user of binding (see benchmark8) and a higher one by async mechanisms capturing and restoring the bindings (benchmark1-7):

https://gist.github.com/whilo/a8ef2cd3f0e033d3973880a2001be32a

I would provide patches to ClojureScript, if this looks like a worthwhile approach.

Environment

Any cljs version.

Activity

Show:
import
August 16, 2016, 10:05 PM

Comment made by: whilo

I have updated the gist to incorporate cljs-zones in benchmark 8, 9 and 10. It is a bit faster in restoring dynamic bindings, but not much:

https://gist.github.com/whilo/a8ef2cd3f0e033d3973880a2001be32a

But there is a significant performance penalty on var access:

full.binding_test.benchmark10()
core.cljs:150 [], ((fn [] v1)), 10000 runs, 1 msecs
core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 4 msecs
null
full.binding_test.benchmark10()
core.cljs:150 [], ((fn [] v1)), 10000 runs, 2 msecs
core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 6 msecs
null
full.binding_test.benchmark10()
core.cljs:150 [], ((fn [] v1)), 10000 runs, 3 msecs
core.cljs:150 [], ((fn [] (zones/get v1))), 10000 runs, 4 msecs

Is this penalty addressable?

One other issue is that JavaScript prototype manipulations through "this" would interfere with the cljs binding mechanism, but this might be acceptable.

Antonin Hildebrand
August 16, 2016, 10:17 PM

Thanks for measuring it. I didn't really get to benchmarking yet.

Did you run the benchmarks under :advanced optimizations?

zones/get currently emits a call to goog.object/get, I'm not sure if this gets inlined by closure compiler, but if not, we can probably improve it by generating raw js object access:
https://github.com/binaryage/cljs-zones/blob/fdfa1421c39d64c9c6b9efbff474b7677f908197/src/lib/zones/core.clj#L94

> One other issue is that JavaScript prototype manipulations through "this" would interfere with the cljs binding mechanism
Can you elaborate? I don't understand how it would interfere.

import
August 17, 2016, 10:10 AM

Comment made by: whilo

With advanced compilation I get:

full.binding_test.benchmark10()
client.js:278 [], ((fn [] v1)), 10000 runs, 1 msecs
client.js:278 [], ((fn [] (zones/get v1))), 10000 runs, 2 msecs
null
full.binding_test.benchmark10()
client.js:278 [], ((fn [] v1)), 10000 runs, 2 msecs
client.js:278 [], ((fn [] (zones/get v1))), 10000 runs, 3 msecs
null
full.binding_test.benchmark10()
client.js:278 [], ((fn [] v1)), 10000 runs, 3 msecs
client.js:278 [], ((fn [] (zones/get v1))), 10000 runs, 2 msecs
null
full.binding_test.benchmark10()
client.js:278 [], ((fn [] v1)), 10000 runs, 1 msecs
client.js:278 [], ((fn [] (zones/get v1))), 10000 runs, 2 msecs
null
full.binding_test.benchmark10()
client.js:278 [], ((fn [] v1)), 10000 runs, 1 msecs
client.js:278 [], ((fn [] (zones/get v1))), 10000 runs, 3 msecs
null
full.binding_test.benchmark10()
client.js:278 [], ((fn [] v1)), 10000 runs, 1 msecs
client.js:278 [], ((fn [] (zones/get v1))), 10000 runs, 1 msecs

But this is a very simple test. If you see a better way to benchmark it, go ahead . When the chains get prototype chains get longer, it might be more painful to walk the chain for each global lookup.

> Can you elaborate? I don't understand how it would interfere.
There has been a proposal somewhere to pass "this" in all cljs functions instead of null as first argument. Your explicit model of a zone is pretty safe I think, but if people would interefere with prototypes in JS, then this might break the "this" approach in subtle ways. Without passing "this" as a direct argument and doing it in a more JS way, you have to setup the zones all the time instead of using the scope chain, which might also contribute to the managing cost. So the "this" approach might help with performance, but I am not sure. You still have the chain overhead. But I am no JS expert, you know much better than me what you are doing.

import
August 20, 2016, 11:37 AM

Comment made by: whilo

My last comment was a little bit confusing. I only see the problem with an impact on dereferencing vars. In the benchmark above the prototype chain is very short (v1 can be directly resolved) and already has a significant penalty, but I think the penalty gets even worse for not rebound root vars if you have a higher nesting level for the binding and the chain gets longer. Can you address this somehow?

David Nolen
May 9, 2017, 2:24 PM

We're just not interested in pursuing this at this time. If at some future point Clojure itself decides to something about this we'll consider it.

Declined

Assignee

Unassigned

Reporter

import

Labels

Approval

None

Patch

None

Affects versions

Priority

Minor