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

欢迎!请参阅 关于 页面了解更多关于这个网站如何工作的信息。

0
Clojure
在我的实践中,使用小于/大于运算符的三参数重载相当常见,例如检查一个数字是否在范围内


(< 0 temp 100)


问题在于,这比 {{(and (< 0 temp) (< temp 100))}} 运算快近三倍。

这是因为三参数重载由通用的可变参数重载分支处理


(defn <
  "如果 nums 是单调递增的,返回非 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)
    


此补丁为以下函数添加了对三参数重载的特殊处理:{{< <= > >= = == not=}}


(defn <
  "如果 nums 是单调递增的,返回非 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 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%)


更高阶的重载也变得更快,主要是因为现在迭代次数少了一次


(= 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%)


此补丁还将 {{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%)


我也在像 [~wagjo] 在 CLJ-1912 中做的那样(只计算一次 {{(next more)}}),尽管单从这个更改中获得性能提升并不大。

我的观点是,优化三个参数很有意义,因为它们在真实代码中出现的频率很高。更高阶数的参数(4个或更多)分布得少得多。

9 答案

0
by

评论者:tonsky

基准测试代码在此 https://gist.github.com/tonsky/442eda3ba6aa4a71fd67883bb3f61d99

0
by

评论者:alexmiller

将此与 CLJ-1912 结合使用可能更合理,否则这些补丁将相互冲突。

0
by

评论者:tonsky

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

0
by

评论者:tonsky

我发现之前的补丁中有一个问题,在定义 {{=}}(相等)时,{{and}} 还未定义。已用 {{if}} 替换。

0
by

评论者:alexmiller

与 CLJ-1912 重复

0
评论:由:tonsky_

[~alexmiller] 这是一个重复项,但我的补丁要快得多。只需看看这些数字(与5-10%相比提高了70-80%)。这是因为我引入了真实的arity,因此在3个参数的情况下不会创建和破坏中间集合。
0

评论:由:wagjo

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

0
评论:由:tonsky_

[~wagjo] 感谢您的发现!附上更新的路径。基准测试并没有产生太大误差,因为性能增益并不是来自更少的/更多比较,而是来自没有调用未知arity的函数的优势。
0
参考:[a href="https://clojure.atlassian.net/browse/CLJ-2075" rel="nofollow" target="_blank">https://clojure.atlassian.net/browse/CLJ-2075
...