2024 Clojure状态调查! 中分享您的想法。

欢迎!请参阅关于页面以获取更多关于此平台如何运作的信息。

0
Clojure by
在我的实践中,使用三元比较运算符(例如检查一个数字是否在范围内)非常常见。


(< 0 temp 100)


问题是,与 {{(and (< 0 temp) (< temp 100))}} 相比,它几乎慢了三倍。

这是因为三元arity是通过通用vararg arity分支处理的


(defn <
  "如果数字按单调递增顺序排列,则返回非nil,否则返回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)
     false)


此补丁为这些函数添加对三元arity的特殊处理:{{< <= > >= = == not=}}


(defn <
  "如果数字按单调递增顺序排列,则返回非nil,否则返回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]
  ([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)))


性能提升相当显著


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


更高阶arity也变得更快,主要是因为现在有一个更少的迭代次数。


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


此补丁还更改了{{not=}}的变长参数形式,改为使用next/recur而不是{{apply}}


(defn not=
  "与(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)))


结果很好


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


我在这里也想像CLJ-1912中的[~wagjo]一样,只计算一次{{(next more)}},虽然仅仅这样做性能提升并不大。

我的观点是,优化三参数函数是有意义的,因为它们在真实代码中出现的频率相当高。参数更多(4个及以上)的函数应用较少。

9 个答案

0

评论者:tonsky

在此处查看基准代码https://gist.github.com/tonsky/442eda3ba6aa4a71fd67883bb3f61d99

0

评论者:alexmiller

将其与CLJ-1912合并可能更合理,否则这些补丁将相互竞争。

0

评论者:tonsky

如果先应用CLJ-1912,请使用此补丁

0

评论者:tonsky

我发现上一个版本的问题,在定义{{=}}(相等性)时,{{and}}尚未定义。用{{if}}替换。

0

评论者:alexmiller

CLJ-1912的重复项

0
_由:tonsky_发表的评论

[~alexmiller] 这是一个重复项,但是我的补丁运行速度要快得多。只需看看数字(比5-10%提高了70-80%)。这是因为我引入了真正的arity,使得在三个参数的情况下,不会创建中间收集以及不会进行解构。
0

评论由:wagjo

提供的补丁(们)中有一个相当严重的错误,例如 {{(= 3 3 2)}} 会返回 true。因此,我想基准测试也出现了问题。

0
_由:tonsky_发表的评论

[~wagjo] 感谢你发现这个问题!附上一个更新后的补丁。性能提升并不是因为减少了一次/多一次比较,而是因为没有调用未知arity的函数所带来的开销。
0
参考:[CLJ-2075](https://clojure.atlassian.net/browse/CLJ-2075)(由tonsky报告)
...