Skip to content

Commit 01af1cf

Browse files
borkdudeswannodette
authored andcommitted
CLJS-3452: optimize str by compiling to + / .toString + compile time optimizations
- Avoiding Array.join in favor of + gives around 4x perf boost - Avoiding 1-arity str calls for compile time constants
1 parent e46a486 commit 01af1cf

File tree

5 files changed

+57
-16
lines changed

5 files changed

+57
-16
lines changed

src/main/cljs/cljs/core.cljs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3110,7 +3110,7 @@ reduces them without incurring seq initialization"
31103110
([] "")
31113111
([x] (if (nil? x)
31123112
""
3113-
(.join #js [x] "")))
3113+
(.toString x)))
31143114
([x & ys]
31153115
(loop [sb (StringBuffer. (str x)) more ys]
31163116
(if more

src/main/clojure/cljs/core.cljc

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -866,23 +866,29 @@
866866
(apply core/str))]
867867
(string-expr (list* 'js* (core/str "[" strs "].join('')") x ys)))))
868868

869+
(core/defn- compile-time-constant? [x]
870+
(core/or
871+
(core/string? x)
872+
(core/keyword? x)
873+
(core/boolean? x)
874+
(core/number? x)))
875+
869876
;; TODO: should probably be a compiler pass to avoid the code duplication
870877
(core/defmacro str
871-
([] "")
872-
([x]
873-
(if (typed-expr? &env x '#{string})
874-
x
875-
(string-expr (core/list 'js* "cljs.core.str.cljs$core$IFn$_invoke$arity$1(~{})" x))))
876-
([x & ys]
877-
(core/let [interpolate (core/fn [x]
878-
(if (typed-expr? &env x '#{string clj-nil})
879-
"~{}"
880-
"cljs.core.str.cljs$core$IFn$_invoke$arity$1(~{})"))
881-
strs (core/->> (core/list* x ys)
882-
(map interpolate)
883-
(interpose ",")
884-
(apply core/str))]
885-
(string-expr (list* 'js* (core/str "[" strs "].join('')") x ys)))))
878+
[& xs]
879+
(core/let [interpolate (core/fn [x]
880+
(core/cond
881+
(typed-expr? &env x '#{clj-nil})
882+
nil
883+
(compile-time-constant? x)
884+
["+~{}" x]
885+
:else
886+
;; Note: can't assume non-nil despite tag here, so we go through str 1-arity
887+
["+cljs.core.str.cljs$core$IFn$_invoke$arity$1(~{})" x]))
888+
strs+args (keep interpolate xs)
889+
strs (string/join (map first strs+args))
890+
args (map second strs+args)]
891+
(string-expr (list* 'js* (core/str "(\"\"" strs ")") args))))
886892

887893
(core/defn- bool-expr [e]
888894
(vary-meta e assoc :tag 'boolean))

src/test/cljs/cljs/core_test.cljs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1957,3 +1957,11 @@
19571957
(is (= "1two:threefour#{:five}[:six]#{:seven}{:eight :nine}"
19581958
(apply cljs.core/str_ 1 ["two" :three 'four #{:five} [:six] #{:seven} {:eight :nine}])))
19591959
(is (= "1234" (apply cljs.core/str_ 1 2 [3 4]))))
1960+
1961+
(deftest test-cljs-3452
1962+
(let [obj #js {:valueOf (fn [] "dude")
1963+
:toString (fn [] "correct")}
1964+
str-fn (fn [x y]
1965+
(str x obj y "\"foobar\"" 1 :foo nil))]
1966+
(testing "object is stringified using toString"
1967+
(is (= "correct6\"foobar\"1:foo" (str-fn nil (+ 1 2 3)))))))
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
(ns cljs-3452-str-optimizations.core)
2+
3+
(defn my-str-fn [x y]
4+
(str x y nil ::foobar "my
5+
6+
multiline
7+
8+
string with `backticks`"
9+
true false 3.14))

src/test/clojure/cljs/build_api_tests.clj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,3 +940,21 @@
940940
(.delete (io/file "package.json"))
941941
(test/delete-node-modules)
942942
(test/delete-out-files out))))
943+
944+
(deftest test-cljs-3452-str-optimizations
945+
(testing "Test that uses compile time optimizations from str macro"
946+
(let [out (.getPath (io/file (test/tmp-dir) "cljs-3452-str-optimizations-out"))]
947+
(test/delete-out-files out)
948+
(let [{:keys [inputs opts]} {:inputs (str (io/file "src" "test" "cljs_build"))
949+
:opts {:main 'cljs-3452-str-optimizations.core
950+
:output-dir out
951+
:optimizations :none
952+
:closure-warnings {:check-types :off}}}
953+
cenv (env/default-compiler-env)]
954+
(build/build (build/inputs (io/file inputs "cljs_3452_str_optimizations/core.cljs")) opts cenv))
955+
(let [source (slurp (io/file out "cljs_3452_str_optimizations/core.js"))]
956+
(testing "only seven string concats, compile time nil is ignored"
957+
(is (= 7 (count (re-seq #"[\+]" source)))))
958+
(testing "only two 1-arity str calls, compile time constants are optimized"
959+
(is (= 2 (count (re-seq #"\$1\(.*?\)" source))))))
960+
(test/delete-out-files out))))

0 commit comments

Comments
 (0)