On notification proxies
Le 05/02/2013 13:52, Sam Tobin-Hochstadt a écrit :
On Tue, Feb 5, 2013 at 7:03 AM, David Bruant<bruant.d at gmail.com> wrote:
I like the current API better because it allows for a cleaner pairing of pre and post-traps, including the ability to share private intermediate state through closure capture. I have to admit, I'm a bit sad to loose that too. But that's the price to pay to get rid of invariant checks I think. It remains possible for pre/post trap to share info through the handler. I've been holding off on this because I know that Mark is still working on notification proxies, but I think this short discussion encapsulates exactly why notification proxies are a bad idea. The big win of notification proxies is that it reduces the cost and complexity of invariant checks [1]. However, I believe this cost is small, and more importantly, not that relevant.
Another justification from my experience is that a lot of traps end with: return Reflect.trap(...args) So notification proxies also make implicit what is otherwise boilerplate. That's an important point (more below).
As evidence that this cost is small, in our work on chaperones in Racket [2], a system that's very similar to proxies [3], we measured the overhead of the invariant checking required. In real programs, even when the proxy overhead was more than half the total runtime, the invariant checks never went above 1% of runtime. Further, the design of chaperones, in combination with much greater use of immutable data in Racket, means that many more invariant checks were performed than would be in a comparable JS system, and the Racket invariant checks would be significantly more expensive.
Interesting stats. Thanks for sharing.
Even more importantly, optimizing the invariant checks is focusing on the wrong use case. Regardless of our preferences, very little JS data is immutable, or requires any invariant checks at all.
At the very least, the engine has to test the following after most traps: target.[GetOwnPropertyDescriptor].[Get] === false If the property is non-configurable, more invariant checks are needed. Otherwise, the code goes on, but it was necessary to test this before knowing it was possible to go on. I'll call this test "pre-invariant check"
So even "no invariant checks" means at least one "pre-invariant check" per-invocation. Since most traps end with "return Reflect.trap(...args)", this test feels even more stupid. Here is what happens in the getOwnPropertyDescriptor trap case:
- call the trap. It most likely ends with "return Reflect.getOwnPropertyDescriptor(...args)"
- The runtime does its own Reflect.getOwnPropertyDescriptor(...args) call and compare its result with the one returned from the trap.
- It obviously notices both descriptors are compatible (duh! they are the same because no code could modify it between the trap return and pre-invariant check)
Most traps have an equivalent story. Only the first step changes because it's a different trap, but by design of the invariants, the "duh!" in 3) remains.
We might count on static analysis or such, but I've been told enough on the list not to rely too much on that. Opinions on that are welcome.
We spend a lot of time focusing on re-implementations of built-in ES and DOM APIs, which often are non-configurable or non-writable, but this is not the common case in user-written JS. Whether it's building free-standing exotic objects or wrapping existing ones, it's very likely that this will continue to be the case with proxy-implemented objects. We should focus on that case.
I could not agree more. For me, getting rid of invariant checks mostly means getting rid of the above test. Since most of my objects don't need invariant checks (because they end with "return Reflect.trap"), I don't know why I should be paying the above test every single time a trap exits. I already know what I want, I made it clear in my code, why am I paying a tax at all, even 0.5%?
In these common cases, I believe that notification proxies are at a significant disadvantage. Notification proxies require that all communication between the handler and the result of an operation operates via mutation of the target. This has several problems. First, it's a tricky pattern that every proxy programmer has to learn, increasing the burden for an already complex API.
I'm balanced on that point. When writing a set trap, the trap is likely to end with "Reflect.set(target, name, value, receiver)", so when I write handler code, I already naturally communicate with the target. But, I agree that there are cases where code returning a different value will have to set the value to the target. I'm not entirely happy with this, but I wonder if it's because I'm just used to direct proxies. In the "notification proxy" way of thinking, traps are a notification mechanism; their return value doesn't matter, so maybe it's normal that communication with the outside occurs through the target.
In direct proxies, the engine has to fetch information on the target (whether to check/pre-check invariants or to return a value). It is not absurd to ask the programmer to put explicitly on the target what the engine should find in it.
Second, it forces the use of the "shadow target" pattern in any wrapper, doubling the number of allocations required.
I don't understand why more shadow targets would be necessary than with direct proxies.
Third, the complexity of this pattern will make proxies that use it harder for engines to optimize. Again, our experience with Racket is relevant here, and we were able to achieve a 4.5x speedup on microbenchmarks (a bubble-sort of a proxied array) by adding simple support in the JIT for proxies.
Isn't it too early to say this pattern is hard to optimize? or harder than direct proxies?
Le 05/02/2013 16:29, David Bruant a écrit :
Le 05/02/2013 13:52, Sam Tobin-Hochstadt a écrit :
Second, it forces the use of the "shadow target" pattern in any wrapper, doubling the number of allocations required. I don't understand why more shadow targets would be necessary than with direct proxies.
Sorry for the very late understanding, but I finally get it. In the case of a membrane, the object used as target needs to have the wrapped objects as property values, which means one of the following:
- change the actual target in the pre-trap and change it back in the post trap. This back and forth has to be done at every property access.
- shadow target
Hmm... actually, because of constraints of the getPrototypeOf trap, membranes implementation has to (lazily) duplicate the entire graph of reachable objects.
In cases where it'd be acceptable to share prototypes (because they'd be frozen and hold no powerful reference, for instance), one can wonder if
- is that cheaper than the invariants notification proxies are meant to remove (add+remove prop and enter/exit 2 function calls)
2013/3/5 David Bruant <bruant.d at gmail.com>
In cases where it'd be acceptable to share prototypes (because they'd be frozen and hold no powerful reference, for instance), one can wonder if 1) is that cheaper than the invariants notification proxies are meant to remove (add+remove prop and enter/exit 2 function calls)
The way I look at Notification Proxies, their benefit is not so much in getting rid of the runtime cost of checking the invariants, the more important virtue is that they get rid of the complexity cost of specifying the invariants (and the associated risk of forgetting to check a crucial invariant).
On a related note, Mark and I just had a paper accepted on the design principles behind direct proxies: soft.vub.ac.be/Publications/2013/vub-soft-tr-13-03.pdf
The paper explains the "shadow target" pattern needed for membranes in-depth (Section 4.3). As Sam mentioned previously, notification proxies don't get rid of this pattern (in fact they may need to make use of it more).
On Tue, Feb 5, 2013 at 7:03 AM, David Bruant <bruant.d at gmail.com> wrote:
I've been holding off on this because I know that Mark is still working on notification proxies, but I think this short discussion encapsulates exactly why notification proxies are a bad idea. The big win of notification proxies is that it reduces the cost and complexity of invariant checks [1]. However, I believe this cost is small, and more importantly, not that relevant.
As evidence that this cost is small, in our work on chaperones in Racket [2], a system that's very similar to proxies [3], we measured the overhead of the invariant checking required. In real programs, even when the proxy overhead was more than half the total runtime, the invariant checks never went above 1% of runtime. Further, the design of chaperones, in combination with much greater use of immutable data in Racket, means that many more invariant checks were performed than would be in a comparable JS system, and the Racket invariant checks would be significantly more expensive.
Even more importantly, optimizing the invariant checks is focusing on the wrong use case. Regardless of our preferences, very little JS data is immutable, or requires any invariant checks at all. We spend a lot of time focusing on re-implementations of built-in ES and DOM APIs, which often are non-configurable or non-writable, but this is not the common case in user-written JS. Whether it's building free-standing exotic objects or wrapping existing ones, it's very likely that this will continue to be the case with proxy-implemented objects. We should focus on that case.
In these common cases, I believe that notification proxies are at a significant disadvantage. Notification proxies require that all communication between the handler and the result of an operation operates via mutation of the target. This has several problems. First, it's a tricky pattern that every proxy programmer has to learn, increasing the burden for an already complex API. Second, it forces the use of the "shadow target" pattern in any wrapper, doubling the number of allocations required. Third, the complexity of this pattern will make proxies that use it harder for engines to optimize. Again, our experience with Racket is relevant here, and we were able to achieve a 4.5x speedup on microbenchmarks (a bubble-sort of a proxied array) by adding simple support in the JIT for proxies.
For all of these reasons, I don't think that notification proxies are the right direction to go.
Sam
[1] Tom makes this point explicitly here www.mail-archive.com/[email protected]/msg19506.html [2] www.cs.umd.edu/~sstrickl/chaperones [3] Tom and I discussed our designs many times while both were under development. Note that in some places, the chaperone design is more similar to "action proxies", but importantly the result of traps is always relevant.