Welcome! Please see the About page for a little more info on how this works.

0 votes
in Clojure by
In my practice, using three-arities of less/greater operations is pretty common for e.g. checking a number is in range:


(< 0 temp 100)


The problem is, it is almost three times as slow compared to {{(and (< 0 temp) (< temp 100))}}.

This happens because three-arities are handled by the generic vararg arity branch:


(defn <
  "Returns non-nil if nums are in monotonically increasing order,
  otherwise false."
  {:inline (fn [x y] `(. clojure.lang.Numbers (lt ~x ~y)))
   :inline-arities #{2}
   :added "1.0"}
  ([x] true)
  ([x y] (. clojure.lang.Numbers (lt x y)))
  ([x y & more]
    (if (< x y)
     (if (next more)
       (recur y (first more) (next more))
       (< y (first more)))
     false)


This patch adds special handling for three-arities to these fns: {{< <= > >= = == not=}}


(defn <
  "Returns non-nil if nums are in monotonically increasing order,
  otherwise false."
  {:inline (fn [x y] `(. clojure.lang.Numbers (lt ~x ~y)))
   :inline-arities #{2}
   :added "1.0"}
  ([x] true)
  ([x y] (. clojure.lang.Numbers (lt x y)))
  ([x y z] (and (. clojure.lang.Numbers (lt x y))
                (. clojure.lang.Numbers (lt y z))))
  ([x y z & more]
   (if (< x y)
     (let [nmore (next more)]
       (if nmore
         (recur y z (first more) nmore)
         (< y z (first more))))
     false)))


The performance gains are quite significant:


(= 5 5 5)      24.508635 ns => 4.802783 ns (-80%)
(not= 1 2 3)      122.085793 ns => 21.828776 ns (-82%)
(< 1 2 3)      30.842993 ns => 6.714757 ns (-78%)
(<= 1 2 2)      30.712399 ns => 6.011326 ns (-80%)
(> 3 2 1)      22.577751 ns => 6.893885 ns (-69%)
(>= 3 2 2)      21.593219 ns => 6.233540 ns (-71%)
(== 5 5 5)      19.700540 ns => 6.066265 ns (-69%)


Higher arities also become faster, mainly because there's one less iteration now:


(= 5 5 5 5)      50.264580 ns => 31.361655 ns (-37%)
(< 1 2 3 4)      68.059758 ns => 43.684409 ns (-35%)
(<= 1 2 2 4)      65.653826 ns => 45.194730 ns (-31%)
(> 3 2 1 0)      119.239733 ns => 44.305519 ns (-62%)
(>= 3 2 2 0)      65.738453 ns => 44.037442 ns (-33%)
(== 5 5 5 5)      50.773521 ns => 33.725097 ns (-33%)


This patch also changes vararg artity of {{not=}} to use next/recur instead of {{apply}}:


(defn not=
  "Same as (not (= obj1 obj2))"
  {:tag Boolean
   :added "1.0"
   :static true}
  ([x] false)
  ([x y] (not (= x y)))
  ([x y z] (not (= x y z)))
  ([x y z & more]
   (if (= x y)
     (let [nmore (next more)]
       (if nmore
         (recur y z (first more) nmore)
         (not= y z (first more))))
     true)))


Results are good:


(not= 1 2 3 4)      130.517439 ns => 29.675640 ns (-77%)


I'm also doing what [~wagjo] did in CLJ-1912 (calculating {{(next more)}} just once), although perf gains from that alone are not that big.

My point here is that optimizing three-arities makes sence because they appear in the real code quite often. Higher arities (4 and more) are much less widespread.

9 Answers

0 votes
by

Comment made by: tonsky

Benchmark code here https://gist.github.com/tonsky/442eda3ba6aa4a71fd67883bb3f61d99

0 votes
by

Comment made by: alexmiller

It might make more sense to combine this with CLJ-1912, otherwise these patches will fight.

0 votes
by

Comment made by: tonsky

Use this patch if CLJ-1912 would be applied first

0 votes
by

Comment made by: tonsky

I found a problem with previous patches that during defining {{=}} (equality), {{and}} is not yet defined. Replaced with {{if}}

0 votes
by

Comment made by: alexmiller

Dupe of CLJ-1912

0 votes
by
_Comment made by: tonsky_

[~alexmiller] It is a duplicate, but my patch is waaaaaaaaay faster. Just look at the numbers (70-80% improvement vs 5-10%). It’s because I introduced a real arity so that intermediate collection is not created and is not destructured in case of 3 arguments.
0 votes
by

Comment made by: wagjo

There's a quite serious bug in the supplied patch(es), that causes e.g. {{(= 3 3 2)}} to return true. Because of this the benchmarks are flawed too I guess.

0 votes
by
_Comment made by: tonsky_

[~wagjo] thanks for spotting this! Attaching an updated path. Benchmark wasn’t flawed too much because perf gain comes not from doing one less/one more comparison but from not having an overhead of calling a fn with unknown arity.
0 votes
by
Reference: https://clojure.atlassian.net/browse/CLJ-2075 (reported by tonsky)
Welcome to Clojure Q&A, where you can ask questions and receive answers from members of the Clojure community.
...