@dnolen just noticed you commented about some concerns in https://clojure.atlassian.net/browse/CLJS-3456 but then deleted the comment, is that message still relevant?
@roman01la no because I realized I was looking at the patches in the wrong order - I think we should think about this a bit more, there is no small advantage in following the established perf patterns that Clojure already has.
What you've done is a kind of manual inlining - which is always a little irritating - I was looking at the patch, what is currently being done, and what Clojure does. I was thinking that I was surprised that the existing pattern couldn't be further optimized by the JS engines.
Was also an interesting reminder how much faster JSC is
for ChunkedSeq specifically I think -equiv is going to be significantly faster just with ICounted implemented https://clojure.atlassian.net/browse/CLJS-2471
yeah that one's obvious - will test that one out shortly
Does CLJS optimize this stuff by hoisting the constant collection out of the function?
(defn skip-truth? [tag]
(contains? #{'boolean 'string} tag))I think there's a ticket for that, but no we don't currently do that
@dnolen I don't know if it exists, but I think = can be optimized when one of the operands is known to be a primitive, we can just use === directly instead of going through cljs.core/=
(it's an optimization I did in squint)
e.g.
(str (fn [x] (= 1 x)))
"function (x){\nreturn cljs.core._EQ_.call(null,(1),x);\n}"
This could just become:
return 1 === xHappy to submit a patch for it
> when one of the operands is known to be a primitive But only the first one, right?
ok, but I think stuff like this is better as compiler passes now - the macro approach just isn't that great in general
@p-himik > But only the first one, right? What's the catch here?
@dnolen could do a pass but I wonder what is the trade-off here:
macro hardly causes longer compilation time, pass might be more expensive
macro would be a quickwin in idioms that are very common like (= 1 x)
passes are not slow, macros tends to compose badly and you simply don't have enough information - type inference stuff doesn't flow well in that context
ok, let me do some benchmarks first how much can be gained from this and I'll write a pass if appropriate
https://github.com/clojure/clojurescript/tree/master/src/main/cljs/cljs/analyzer/passes
the and/or fix is a good example of this stuff as macro just not being any good - it composed badly w/ core.async
re doing stuff in macros
I've been looking into folding predicates into constants for non-nil cases (`some?`, nil?, etc), for now I'm not sure how this can be done in the compiler itself since nil? for example is a macro that calls into coercive-= which is a macro as well that expands into (js* "(~{} == ~{})" x y), leaving no info about semantics of the expression.
but doing (ana/infer-tag &env (resolve-var x)) in the macro worked, although I'm not happy with this approach
this probably means we could do w/ some prep work - why not include the original form w/ js*
and make sure in the js* branch of the the analyzer we parse the original form as well
js* is generally pure optimizing - i.e. inlining macro
semantically the original form is equivalent, and we're using js* for juice
here's a simple benchmark:
(def iters 1000000000)
(defn str-test []
(let [f1 (fn [x] (= 1 x))
f2 (fn [x] (identical? 1 x))]
(js/console.log "f1" (str f1))
(js/console.log "f1" (str (fn []
(f1 1))))
(js/console.log "f2" (str f2))
(js/console.log "f2" (str (fn []
(f1 1))))
(simple-benchmark [] (f1 2) iters)
(simple-benchmark [] (f2 2) iters))))
f1 function a(c){return R.h(1,c)}
f1 function(){return a(1)}
f2 function(c){return 1===c}
f2 function(){return a(1)}
[], (f1 2), 1000000000 runs, 4076 msecs
[], (f2 2), 1000000000 runs, 271 msecs
20x speedup seems worth it.this optimization is actually more useful than it might appear
for example I think we might have special cases in case - where if (== ...) worked this way - those special cases would go away
sorry, made a mistake, the second f2 line should be:
(js/console.log "f2" (str (fn []
(f2 1))))
closure make of this:
f2 function(){return!0}
which is very optimal :)nice
right anytime you can lower to primitives, Closure can do more work for us
cool
will do some pass work then
@roman01la if you have cycles, I would definitely take a patch that makes js* not throw away information, I think that opens up some interesting doors later.
often macros preserve js* info by adding metadata to the list right?
yeah but it might be cool to always parse this extra data in the analyzer - and make sure we doing this uniformly
so that we have an established pattern - it's probably a bit ad-hoc right now
@borkdude
> What's the catch here?
If a particular type specifies its own -equiv, you cannot assume that it can be replaced with ===.
you mean that you can have a custom type that says: I'm equal to the primitive 1. good point
but how does order affect that? this could both be true? (= 1 custom) vs (= custom 1) ?
hmm, only in the second case .equiv is called on custom, I see
seems very brittle for some equiv impl to rely on the order of args in =. who on earth would do this
it is true however good or bad it is - that would be a breaking change for libs / apps that rely on this behavior w/ JS primitives.
that is they already redefined equiv for numbers for some reason - an inlining thing would break that so that is a good point @p-himik
but we are in a position to detect that case actually ... since ClojureScript is whole program
Non commutative = should be forbidden
no I'm suggesting they did that work
then inlining would break their work
I know. We should not break. But itβs a very bad idea. Are you suggesting we can detect equiv impls that return true for primitives?
it could be done but it would be warping the compilation model for a very small benefit
I have a clj-kondo linter for writing the expected value first. We could just optimize only that case
optimizing only the expected value first is still a breaking change if they overwrote (extend-type number IEquiv ...) with their own thing
Ok I guess I could just write a clj-kondo linter thatβs going to suggest rewriting to identical? When a primitive is used and be done with it
Or does CLJS have == for triple equals maybe (not at kbd now)
Yes:
(core/defmacro ^::ana/numeric ==
([x] true)
([x y] (bool-expr (core/list 'js* "(~{} === ~{})" x y)))
([x y & more] `(and (== ~x ~y) (== ~y ~@more))))But there's still a non-macro variant that respects -equiv. But it does say that it should be used only for numbers.
(defn ^boolean ==
"Returns non-nil if nums all have the equivalent
value, otherwise false. Behavior on non nums is
undefined."
([x] true)
([x y] (-equiv x y))
([x y & more]
(if (== x y)
(if (next more)
(recur y (first more) (next more))
(== y (first more)))
false)))Right I guess that locks the door for the optimization then, equiv can be overridden for primitives.
@dnolen
> this probably means we could do w/ some prep work - why not include the original form w/ js*
let's say we store :js-op-form (cljs.core/coercive-= x nil) in ast node of the :js op, how we can make use of this info later and at what stage (analyzer or compiler)? I'm trying to think of a concrete example, like folding (nil? x) into a constant boolean. btw we already store :js-op cljs.core/coercive-= in ast.
I can think of having a set of predefined "evaluators", (eval-form 'cljs.core/coercive-= ...args)
ideally this would be a generic mechanism, but we'd have to parse JS string then