Making CLJS output smaller

186 views
Skip to first unread message

Herwig Hochleitner

unread,
Oct 15, 2012, 10:14:13 PM10/15/12
to clo...@googlegroups.com
When investigating what the closure compiler does with cljs output, I discovered the following:

Google closure does not eliminate unused var reads. This means that e.g. a deftype

(deftype A [])

which compiles to

workbench.foo.A = (function (){
    .... snip ....
});
workbench.foo.A; // <-- because of this

does not get stripped out of the compilate if A is never used even with advanced optimizations.

What can we do about this? I tried changing clojurescript's deftype macro to not return the type itself.
This shrank a clojurescript hello world from about 100k to 75k. Quite an improvement for a single line change.

Obviously, we do want deftype to return its generated type, so my next attempt was to eliminate the unused reads with a compiler optimization.
My first cut works by adding an entry point function for an optimizer, which gets passed a single toplevel form from the analyzer at a time.
Output from this modified compiler generates file sizes similar to the version with the modified macro.

Of course we don't want a single static entry point for a hypothetical optimizer. Instead I'd like a variant of the compile function, that takes optimizations as a parameter.
Such optimizations should also be able to operate globally under a closed world assumption with explicit exports.

==

Before implementing a hook to enable whole program optimizations, I'd to get some feedback on my proposal on how to do it:

a) Have the compiler gain a notion of a compilation unit, expressed as a sequence of top level forms.
Compilation units are conceptionally similar, but orthogonal, to name spaces. A CU can be as small as a form on the REPL and as big as a whole application.
In real world applications, namespaces and compilation units will often align.

b) Compilation units are embodied as a function (compile-unit env forms dst optimization) in cljs.compiler, which returns an updated env and serves as the base for all other compilation functions.
optimization is a function, that gets passed the sequence of analyzed toplevel forms and returns a possibly optimized version, which is passed to emit.

c) cljs.optimizer contains common code like builtin optimizations, optimization combinators and analysis tree walkers. Each optimization checks its own preconditions (assign once? no eval? no var? ... getting ahead of myself)

Is there interest in including hooks for optimizations in CLJS? If so, is the approach outlined above reasonably straightforward or do we need a confluence page?

kind regards

David Nolen

unread,
Oct 15, 2012, 11:16:43 PM10/15/12
to clo...@googlegroups.com
On Mon, Oct 15, 2012 at 10:14 PM, Herwig Hochleitner <hhochl...@gmail.com> wrote:
When investigating what the closure compiler does with cljs output, I discovered the following:

Google closure does not eliminate unused var reads. This means that e.g. a deftype

(deftype A [])

which compiles to

workbench.foo.A = (function (){
    .... snip ....
});
workbench.foo.A; // <-- because of this

does not get stripped out of the compilate if A is never used even with advanced optimizations.

What can we do about this? I tried changing clojurescript's deftype macro to not return the type itself.
This shrank a clojurescript hello world from about 100k to 75k. Quite an improvement for a single line change.

Yep I think there are quite a few things like this. But I don't think we need an optimization pass for this paticular case (and I'm not saying that's not a good idea - see below). Hopefully we can a direct patch for this issue around top level deftypes/records.

Is there interest in including hooks for optimizations in CLJS? If so, is the approach outlined above reasonably straightforward or do we need a confluence page?

kind regards

Definitely needs a Confluence page. I know several people are interested in this. I think it would be pretty sweet to provide common optimizations out of the box.

David 

Herwig Hochleitner

unread,
Oct 16, 2012, 7:19:39 AM10/16/12
to clo...@googlegroups.com
2012/10/16 David Nolen <dnolen...@gmail.com>

Yep I think there are quite a few things like this. But I don't think we need an optimization pass for this paticular case (and I'm not saying that's not a good idea - see below). Hopefully we can a direct patch for this issue around top level deftypes/records.

Certainly, such a simple optimization (omitting var reads in a statement context) could live in the emitter. 
The next stumbling block to a smaller clojurescript, however, is the global-hierarchy var, which doesn't get removed by the closure compiler. To shake that var, some closed world optimization could be utilized. I'd be happy to work on that, but I need compilation units in order to do that.

Another use case for compilation units I can think of from the top of my head are constant pools.

Definitely needs a Confluence page. I know several people are interested in this. I think it would be pretty sweet to provide common optimizations out of the box. 
 
Seems like I can't edit Confluence. Could somebody elevate my privileges?

Is it OK if I create a page "Compilation Units"?

kind regards

David Nolen

unread,
Oct 16, 2012, 8:26:59 AM10/16/12
to clo...@googlegroups.com
On Tue, Oct 16, 2012 at 7:19 AM, Herwig Hochleitner <hhochl...@gmail.com> wrote:
Certainly, such a simple optimization (omitting var reads in a statement context) could live in the emitter. 
The next stumbling block to a smaller clojurescript, however, is the global-hierarchy var, which doesn't get removed by the closure compiler. To shake that var, some closed world optimization could be utilized. I'd be happy to work on that, but I need compilation units in order to do that.

I'm aware of this one as well. But again I think we can and should do a quick fix in the compiler for this. Either the user used multimethods or they did not.

These kind of simple approaches may seem "hacky" but I think the benefit to users in terms of code size today outweighs those concerns - the discussed fixes would be quite simple to remove when a more general optimization strategy is in place.

Herwig Hochleitner

unread,
Oct 16, 2012, 12:27:54 PM10/16/12
to clo...@googlegroups.com
2012/10/16 David Nolen <dnolen...@gmail.com>

I'm aware of this one as well. But again I think we can and should do a quick fix in the compiler for this. Either the user used multimethods or they did not.

I don't see how such a quick fix could be done, without the compiler gaining a concept of "the whole program", in which the user could have used MMs.
In my understanding, the compiler, as it is right now, is fundamentally form-at-a-time, so at the point the global-hierarchy var already gets emitted, you can't possibly know if it will be used.
Certainly, you are much more familiar with the implementation, do you see a point where this could already be hacked in?
 
These kind of simple approaches may seem "hacky" but I think the benefit to users in terms of code size today outweighs those concerns - the discussed fixes would be quite simple to remove when a more general optimization strategy is in place. 
 
To be honest, the introduction of compile-unit is the simplest, most general solution I could come up with after some hammock time. Since it seems not to be an obiously bad idea, I'll stop here and get back to confluence with a fully thought out  proposal + implementation. Feedback and alternate approaches still welcome.

2012/10/16 Andy Fingerhut <andy.fi...@gmail.com>
Herwig:

I've added you to a couple of groups (clojure-dev and jira-developers) on Confluence that you did not previously belong to.  I'm not sure, but this may give you permission to edit Confluence.  Let me know if you still cannot do so after receiving this message.

Thank you! Unfortunately that didn't seem to help. In the top bar I have 2 menus "Browse" and my account. The tools menu on each page doesn't let me edit either.

kind regards

David Nolen

unread,
Oct 16, 2012, 12:39:26 PM10/16/12
to clo...@googlegroups.com
On Tue, Oct 16, 2012 at 12:27 PM, Herwig Hochleitner <hhochl...@gmail.com> wrote:
2012/10/16 David Nolen <dnolen...@gmail.com>

I'm aware of this one as well. But again I think we can and should do a quick fix in the compiler for this. Either the user used multimethods or they did not.

I don't see how such a quick fix could be done, without the compiler gaining a concept of "the whole program", in which the user could have used MMs.
In my understanding, the compiler, as it is right now, is fundamentally form-at-a-time, so at the point the global-hierarchy var already gets emitted, you can't possibly know if it will be used.
Certainly, you are much more familiar with the implementation, do you see a point where this could already be hacked in?

ClojureScript programmers benefit from the assumption of whole program optimization for production code. Also remember we have analyze-file.

If these weren't true ClojureScript would not be anywhere near as fast as it currently is. Look at all the assumptions we can make when we compile a fn invocation form under advanced compilation.

David

Herwig Hochleitner

unread,
Oct 16, 2012, 1:25:35 PM10/16/12
to clo...@googlegroups.com
2012/10/16 David Nolen <dnolen...@gmail.com>

ClojureScript programmers benefit from the assumption of whole program optimization for production code. Also remember we have analyze-file.


They most certainly do, but in my mind this assumption is only made in the google closure compiler, right now.
I see analyze-file is used in the compiler to prepopulate an analysis environment with clojure.core. It still doesn't look at the whole program at once.
IMO it wouldn't be a good fit to base optimizations on, since that would hardcode a compilation unit to be a file. Also, you couldn't remove multimethod infrastructure with it, since it would only know about a file at a time.
 
If these weren't true ClojureScript would not be anywhere near as fast as it currently is. Look at all the assumptions we can make when we compile a fn invocation form under advanced compilation.


Are you are talking about compile time optimization for invokations of fixed arity, protocols, keywords and clojure.core/not, by chance? Those are fine optimizations, but they don't depend on WPO or advanced mode.
In my mental model, the cljs compiler generates the same intermediate javascript in all optimization levels. I have looked at cljs.analyzer/parse-invoke and cljs.compiler/emit :invoke but couldn't find any different code paths. Am I missing something?

David Nolen

unread,
Oct 16, 2012, 1:39:05 PM10/16/12
to clo...@googlegroups.com
None of the invocation optimizations make any sense for anything other that advanced optimization. They hard code static assumptions that might get invalidated in any compilation unit other than whole program.

We don't need to look at the whole program at once. Think about why we need declare and most Clojure programs avoid it if they can.

I don't think we need any fancy compiler stuff to fix the two big dead code issues in the near term.
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Herwig Hochleitner

unread,
Oct 16, 2012, 2:41:11 PM10/16/12
to clo...@googlegroups.com
2012/10/16 David Nolen <dnolen...@gmail.com>
None of the invocation optimizations make any sense for anything other that advanced optimization.

But the invokations in javascript output work even when not using google closure at all, e.g. the REPL.
 
They hard code static assumptions that might get invalidated in any compilation unit other than whole program.

Partially agree: I would call those hard coded static assumptions an ABI (e.g. the naming convention for fixed arity invokations, or the invokation of a protocol fn) and compilation units would only work, when the APIs they consume have a matching ABI (this is true even now, you wouldn't compile parts of your project with different versions of clojurescript, would you?).
 
We don't need to look at the whole program at once. Think about why we need declare and most Clojure programs avoid it if they can.


We need declare, because clojure features a single pass compiler, and sometimes we want to code mutual recursion without letfn. The moral equivalent for the MM dead code issue would be a compiler flag to turn off multimethods, right?. Certainly doable. If you'd like me to implement that flag before turning to more comprehensive optimization, I'll do it.

So yes, if we want to eliminate more dead code without having the user manually turn off features, I think we do need to look at the whole program, don't we?

Please also note: I'm not proposing to get rid of the single pass semantics of clojure. Quite the opposite. After the analyzer is done with its single pass and has unambigously resolved everything, optimization passes are free to optimize, since the semantics are already established.
 
I don't think we need any fancy compiler stuff to fix the two big dead code issues in the near term.


I agree that the unused read issue can be generically solved in the emitter. For the one with unused toplevel vars like global-hierachy, I'm not so sure, especially if we also want to solve the problem for libraries, which arguably is a goal of clojurescript.

David Nolen

unread,
Oct 16, 2012, 3:00:04 PM10/16/12
to clo...@googlegroups.com
On Tue, Oct 16, 2012 at 2:41 PM, Herwig Hochleitner <hhochl...@gmail.com> wrote:
2012/10/16 David Nolen <dnolen...@gmail.com>
None of the invocation optimizations make any sense for anything other that advanced optimization.

But the invokations in javascript output work even when not using google closure at all, e.g. the REPL.

Yes because we don't try to optimize them.
 
 
They hard code static assumptions that might get invalidated in any compilation unit other than whole program.

Partially agree: I would call those hard coded static assumptions an ABI (e.g. the naming convention for fixed arity invokations, or the invokation of a protocol fn) and compilation units would only work, when the APIs they consume have a matching ABI (this is true even now, you wouldn't compile parts of your project with different versions of clojurescript, would you?).

Start a CLJS REPL with :static-fns true. Make a function called foo. Call it from a function bar. Redef foo w/ different arities. Now call bar. This will not work. Same if you redef foo to be an instance of a deftype that implements IFn.
 
We need declare, because clojure features a single pass compiler, and sometimes we want to code mutual recursion without letfn. The moral equivalent for the MM dead code issue would be a compiler flag to turn off multimethods, right?. Certainly doable. If you'd like me to implement that flag before turning to more comprehensive optimization, I'll do it.

Either the user used multimethods or they did not. If they did emit the hierarchy. And only bother with this code size optimization during advanced compilation - just always emit the hierarchy otherwise.
 
Please also note: I'm not proposing to get rid of the single pass semantics of clojure. Quite the opposite. After the analyzer is done with its single pass and has unambigously resolved everything, optimization passes are free to optimize, since the semantics are already established.

Nothing I've said has anything do w/ optimization passes. Just how immediate problems could be solved without waiting for that ;) 

Herwig Hochleitner

unread,
Oct 16, 2012, 5:38:53 PM10/16/12
to clo...@googlegroups.com
2012/10/16 David Nolen <dnolen...@gmail.com>

But the invokations in javascript output work even when not using google closure at all, e.g. the REPL.

Yes because we don't try to optimize them.

And suppose one tried to optimize them: With an established concept of compilation units, it's straightforward to optimize invokations within a unit and keep a compatible invokation ABI of its public members with following effect:

- single form units (REPL) work
- whole program units (Clojurescript) work
- namespace/file units (Clojurescript with a JVM backend, hypothetically) work

Again, getting ahead of myself here.
 
Start a CLJS REPL with :static-fns true. Make a function called foo. Call it from a function bar. Redef foo w/ different arities. Now call bar. This will not work. Same if you redef foo to be an instance of a deftype that implements IFn.

Thanks for raising that point. So IFn call is different from an aritiy call is different from a protocol call in clojurescripts informally defined ABI (with :static-fns). This has to be considered when thinking about compilation units.
 
We need declare, because clojure features a single pass compiler, and sometimes we want to code mutual recursion without letfn. The moral equivalent for the MM dead code issue would be a compiler flag to turn off multimethods, right?. Certainly doable. If you'd like me to implement that flag before turning to more comprehensive optimization, I'll do it.

Either the user used multimethods or they did not. If they did emit the hierarchy. And only bother with this code size optimization during advanced compilation - just always emit the hierarchy otherwise.

The emitter still can not look ahead in its single pass, but thinking about it, it seems to me your proposal could be implemented by lazily initializing global-hierachy in derive.
 
Please also note: I'm not proposing to get rid of the single pass semantics of clojure. Quite the opposite. After the analyzer is done with its single pass and has unambigously resolved everything, optimization passes are free to optimize, since the semantics are already established.

Nothing I've said has anything do w/ optimization passes. Just how immediate problems could be solved without waiting for that ;) 
 
Ack, I think I'll try the two simple fixes first ;)

David Nolen

unread,
Oct 16, 2012, 5:57:13 PM10/16/12
to clo...@googlegroups.com
On Tue, Oct 16, 2012 at 5:38 PM, Herwig Hochleitner <hhochl...@gmail.com> wrote:

The emitter still can not look ahead in its single pass, but thinking about it, it seems to me your proposal could be implemented by lazily initializing global-hierachy in derive.

Sounds good!
 
 
Please also note: I'm not proposing to get rid of the single pass semantics of clojure. Quite the opposite. After the analyzer is done with its single pass and has unambigously resolved everything, optimization passes are free to optimize, since the semantics are already established.

Nothing I've said has anything do w/ optimization passes. Just how immediate problems could be solved without waiting for that ;) 
 
Ack, I think I'll try the two simple fixes first ;)

;)

Herwig Hochleitner

unread,
Oct 19, 2012, 2:29:43 AM10/19/12
to clo...@googlegroups.com
I've implemented the fixes as suggested and created 3 tickets with patches:

Except for the new js rendered by stack traces on the repl, this should be a no brainer

2) Create a new macro extend-instance http://dev.clojure.org/jira/browse/CLJS-398
Allows you to extend a protocol to single objects
I needed this for the lazy atom impl, but it is much more general
I'm also already using something like this in production

3) Lazy init global-hierarchy http://dev.clojure.org/jira/browse/CLJS-399
This backs reset! swap! and compare-and-set! on protocols, which means introducing IPlace for -reset!
Then global-hierarchy is implemented as a lazy atom
Reply all
Reply to author
Forward
0 new messages