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

欢迎!请参阅 关于 页面了解更多关于该功能的信息。

+13
Protocols
目前 `satisfies?` 并未使用与协议方法相同的实现缓存,因此在实际应用中执行速度过慢。

使用

(defprotocol p (f [_]))
(deftype x [])
(deftype y [])
(extend-type x p (f [_]))


修改前

(let [s "abc"] (bench (instance? CharSequence s))) ;; 执行时间平均值:1.358360 ns
(let [x (x.)] (bench (satisfies? p x))) ;; 执行时间平均值:112.649568 ns
(let [y (y.)] (bench (satisfies? p y))) ;; 执行时间平均值:2.605426 µs


*原因*:`satisfies?` 调用 `find-protocol-impl` 以判断对象是否实现了协议,这会检查 x 是否是协议接口的实例或者 x 的类是否是协议实现之一(或者是否在一个会使得这段代码为真的继承链中)。这个检查相对昂贵且没有进行缓存。

*建议*:扩展协议的方法实现缓存以处理(并缓存)实例检查(包括负结果)。

修改后

(let [x (x.)] (bench (satisfies? p x))) ;; 执行时间平均值:79.321426 ns
(let [y (y.)] (bench (satisfies? p y))) ;; 执行时间平均值:77.410858 ns


*修复方案*:CLJ-1814-v7.patch (依赖于 CLJ-2426)

25 个答案

0

由:michaelblume

很好。在我们的重构之前,Honeysql 在 `satisfies?` 调用上花费了 80-90% 的时间。

0

由:michaelblume

我意识到这是一个非常令人生厌的缺陷,但如果你克隆 core.match,将其 Clojure 依赖指向 1.8.0-master-SNAPSHOT,启动一个 REPL,然后通过 vim 连接到 REPL,并重新加载 clojure.core.match,你会得到:

`
|| java.lang.Exception: 命名空间 'clojure.tools.analyzer.jvm.utils' 未找到,编译:(clojure/tools/analyzer/jvm.clj:9:1)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5647| clojure.core$throw_if.invokeStatic
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5733| clojure.core$load_lib.invokeStatic
|| clojure.core$load_lib.doInvoke(core.clj)
|| clojure.lang.RestFn.applyTo(RestFn.java:142)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|647| clojure.core$apply.invokeStatic
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5765| clojure.core$load_libs.invokeStatic
|| clojure.core$load_libs.doInvoke(core.clj)
|| clojure.lang.RestFn.applyTo(RestFn.java:137)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|647| clojure.core$apply.invokeStatic
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5787| clojure.core$require.invokeStatic
|| clojure.core$require.doInvoke(core.clj)
|| clojure.lang.RestFn.invoke(RestFn.java:703)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/tools.analyzer.jvm/0.6.5/tools.analyzer.jvm-0.6.5.jar::clojure/tools/analyzer/jvm.clj|9| clojure.tools.analyzer.jvm$eval4968$loading
zipfile:/Users/michael.blume/.m2/repository/org/clojure/tools.analyzer.jvm/0.6.5/tools.analyzer.jvm-0.6.5.jar::clojure/tools/analyzer/jvm.clj|9| clojure.tools.analyzer.jvm$eval4968.invokeStatic
|| clojure.tools.analyzer.jvm$eval4968.invoke(jvm.clj)
|| clojure.lang.Compiler.eval(Compiler.java:6934)
|| clojure.lang Compiler.eval(Compiler.java:6923)
|| clojure.lang Compiler.load(Compiler.java:7381)
|| clojure.lang.RT.loadResourceScript(RT.java:372)
|| clojure.lang.RT.loadResourceScript(RT.java:363)
|| clojure.lang.RT.load(RT.java:453)
|| clojure.lang.RT.load(RT.java:419)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5883| clojure.core$load$fn
5669 invoking
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5882| clojure.core$load.invokeStatic
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5683| clojure.core$load_one.invokeStatic
|| clojure.core$load_one.invoke(core.clj)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5728| clojure.core$load_lib$fn
5618 invoking
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5727| clojure.core$load_lib.invokeStatic
|| clojure.core$load_lib.doInvoke(core.clj)
|| clojure.lang.RestFn.applyTo(RestFn.java:142)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|647| clojure.core$apply.invokeStatic
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5765| clojure.core$load_libs.invokeStatic
|| clojure.core$load_libs.doInvoke(core.clj)
|| clojure.lang.RestFn.applyTo(RestFn.java:137)
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|647| clojure.core$apply.invokeStatic
zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5787| clojure.core$require.invokeStatic





`

0

评论者:bronsa

AOT是否涉及其中?

0

由:michaelblume

缩小了问题范围,如果查看 tools.analyzer.jvm,打开REPL,并执行 (require 'clojure.tools.analyzer.jvm.utils),则会得到

|| java.lang.ClassCastException: 无法将 java.lang.Class 转换为 clojure.asm.Type,在编译:(utils.clj:260:13) || clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3642) || clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3636) || clojure.lang.Compiler$DefExpr.eval(Compiler.java:450) || clojure.lang.Compiler.eval(Compiler.java:6939) || clojure.lang.Compiler.load(Compiler.java:7381) || clojure.lang.RT.loadResourceScript(RT.java:372) || clojure.lang.RT.loadResourceScript(RT.java:363) || clojure.lang.RT.load(RT.java:453) || clojure.lang.RT.load(RT.java:419) 从zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5883| clojure.core$load$fn__5669.invoke zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5882| clojure.core$load.invokeStatic zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5683| clojure.core$load_one.invokeStatic || clojure.core$load_one.invoke(core.clj) zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5728| clojure.core$load_lib$fn__5618.invoke zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5727| clojure.core$load_lib.invokeStatic || clojure.core$load_lib.doInvoke(core.clj) || clojure.lang.RestFn.applyTo(RestFn.java:142) zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|647| clojure.core$apply.invokeStatic zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5765| clojure.core$load_libs.invokeStatic || clojure.core$load_libs.doInvoke(core.clj) || clojure.lang.RestFn.applyTo(RestFn.java:137) zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|647| clojure.core$apply.invokeStatic zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|5787| clojure.core$require.invokeStatic || clojure.core$require.doInvoke(core.clj) || clojure.lang.RestFn.invoke(RestFn.java:421) || clojure.tools.analyzer.jvm.utils$eval4392.invokeStatic(form-init8663423518975891793.clj:1) || clojure.tools.analyzer.jvm.utils$eval4392.invoke(form-init8663423518975891793.clj) || clojure.lang.Compiler.eval(Compiler.java:6934) || clojure.lang.Compiler.eval(Compiler.java:6897) zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|3096| clojure.core$eval.invokeStatic || clojure.core$eval.invoke(core.clj) zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/main.clj|240| clojure.main$repl$read_eval_print__7404$fn__7407.invoke zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/main.clj|240| clojure.main$repl$read_eval_print__7404.invoke zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/main.clj|258| clojure.main$repl$fn__7413.invoke zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/main.clj|258| clojure.main$repl.invokeStatic || clojure.main$repl.doInvoke(main.clj) || clojure.lang.RestFn.invoke(RestFn.java:1523) zipfile:/Users/michael.blume/.m2/repository/org/clojure/tools.nrepl/0.2.10/tools.nrepl-0.2.10.jar::clojure/tools/nrepl/middleware/interruptible_eval.clj|58| clojure.tools.nrepl.middleware.interruptible_eval$evaluate$fn__637.invoke || clojure.lang.AFn.applyToHelper(AFn.java:152) || clojure.lang.AFn.applyTo(AFn.java:144) zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|645| clojure.core$apply.invokeStatic zipfile:/Users/michael.blume/.m2/repository/org/clojure/clojure/1.8.0-master-SNAPSHOT/clojure-1.8.0-master-SNAPSHOT.jar::clojure/core.clj|1874| clojure.core$with_bindings_STAR_.invokeStatic || clojure.core$with_bindings_STAR_.doInvoke(core.clj) || clojure.lang.RestFn.invoke(RestFn.java:425) zipfile:/Users/michael.blume/.m2/repository/org/clojure/tools.nrepl/0.2.10/tools.nrepl-0.2.10.jar::clojure/tools/nrepl/middleware/interruptible_eval.clj|56| clojure.tools.nrepl.middleware.interruptible_eval$evaluate.invokeStatic || clojure.tools.nrepl.middleware.interruptible_eval$evaluate.invoke(interruptible_eval.clj) zipfile:/Users/michael.blume/.m2/repository/org/clojure/tools.nrepl/0.2.10/tools.nrepl-0.2.10.jar::clojure/tools/nrepl/middleware/interruptible_eval.clj|191| clojure.tools.nrepl.middleware.interruptible_eval$interruptible_eval$fn__679$fn__682.invoke zipfile:/Users/michael.blume/.m2/repository/org/clojure/tools.nrepl/0.2.10/tools.nrepl-0.2.10.jar::clojure/tools/nrepl/middleware/interruptible_eval.clj|159| clojure.tools.nrepl.middleware.interruptible_eval$run_next$fn__674.invoke || clojure.lang.AFn.run(AFn.java:22) || java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) || java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) || java.lang.Thread.run(Thread.java:745)

我没有看到哪里会涉及到AOT(预先编译)?

0

评论者:bronsa

(链接: ~michaelblume) 更新的补丁应该能修复你报告的问题

0

由:michaelblume

太酷了,谢谢 =)

新补丁不再删除未使用的 MethodImplCache,这是故意的吗?

0

评论由: alexmiller 提出

如果补丁中描述了改变的列表项,那会很好,例如:"将 MethodImplCache 重命名为 ImplCache"。这有助于更容易地进行审查。

0

评论者:bronsa

附上一个更新的补丁,该补丁不替换 MethodImplCache 为 ImplCache,而是简单地重用 MethodImplCache,减少此补丁的影响,并使其更容易(更安全)地审查。

0
_评论由: alexmiller 提出

由于这是在新的 {{inst?}} 谓词中使用,因此提高优先级 - 请参阅 https://github.com/clojure/clojure/commit/58227c5de080110cb2ce5bc9f987d995a911b13e
0
_评论由: alexmiller 提出

我对 v3 补丁前后的测试进行了检索。之前的时间非常吻合,但无法重现之后的测试结果。我得到了这个,实际上在找不到的情况下更糟


(let [x (x.)] (bench (satisfies? p x))) ;; 平均执行时间:76.833504 纳秒
(let [y (y.)] (bench (satisfies? p y))) ;; 平均执行时间:20.570007 微秒

0
_评论由: bronsa_ 提出

v4 补丁解决了找不到情况下的回归问题,不清楚这是怎么发生的,对此表示歉意。
以下是我现在得到的时间

clojure master

用户=> (let [x (x.)] (bench (satisfies? p x)))
评估次数:在60个样本中进行,每个样本10082693次调用,共604961580次。
           平均执行时间:112.649568纳秒
    执行时间标准偏差:12.216782纳秒
   执行时间下四分位数:99.299203纳秒(2.5%)
   执行时间上四分位数:144.265205纳秒(97.5%)
               使用的开销:1.898271纳秒

在60个样本中发现了3个异常值(5.0000%)
    低严重程度     2(3.3333%)
    低轻微程度     1(1.6667%)
 异常值引起的方差:73.7545% 异常值导致方差严重偏大
nil




用户=> (let [y (y.)] (bench (satisfies? p y)))
评估次数:在60个样本中进行,每个样本377935次调用,共22676100次。
             平均执行时间:2.605426微秒
    执行时间标准偏差:141.100070纳秒
   执行时间下四分位数:2.487234微秒(2.5%)
   执行时间上四分位数:2.873045微秒(97.5%)
               使用的开销:1.898271纳秒

在60个样本中发现了1个异常值(1.6667%)
    低严重程度     1(1.6667%)
 异常值引起的方差:40.1251% 异常值导致方差中度偏大
nil



master + v4

用户=> (let [x (x.)] (bench (satisfies? p x)))
评估次数:在60个样本中进行,每个样本12195985次调用,共731759100次。
             平均执行时间:79.321426纳秒
    执行时间标准偏差:3.959245纳秒
   执行时间下四分位数:75.365187纳秒(2.5%)
   执行时间上四分位数:87.986479纳秒(97.5%)
               使用的开销:1.905711纳秒

在60个样本中发现了1个异常值(1.6667%)
    低严重程度     1(1.6667%)
 异常值引起的方差:35.2614% 异常值导致方差中度偏大
nil



用户=> (let [y (y.)] (bench (satisfies? p y)))
评估次数:在60个样本中进行,每个样本12853669次调用,共771220140次。
             平均执行时间:77.410858纳秒
    执行时间标准偏差:1.407926纳秒
   执行时间下四分位数:75.852530纳秒(2.5%)
   执行时间上四分位数:80.759226纳秒(97.5%)
               使用的开销:1.897646纳秒

在60个样本中发现了4个异常值(6.6667%)
    低严重程度     3(5.0000%)
    低轻微程度     1(1.6667%)
 异常值引起的方差:7.7866% 异常值导致方差略偏大


总结如下
master found = 112ns
master not-found = 2.6us

master+v4 found = 79ns
master+v4 not-found = 77ns
0
by

由:michaelblume

对于那些已声明采用特定协议实现的记录,并且因此实现了相应接口的记录,能否使用针对该接口的(实例)检查作为快速路径是有意义的吗?

0
by

评论由: alexmiller 提出

Michael - 那个检查已经在里面了

Nicola - 我有一些评论/问题

  1. 我不明白NIL这些东西的作用——你能解释一下吗?
  2. 在这种情况下,如果x是接口的一个实例,旧代码返回x,而新的find-protocol-impl*代码返回interface。为什么会有这个变化?
  3. 这个:(alter-var-root (:var protocol) assoc :impl-cache (expand-method-impl-cache cache c impl)) 在我看来并不是线程安全的——我认为在两个不同的线程中对于不同的impl同时发生错过会造成缓存中只有其中一个实例。这可能是极不可能的,也可能不是什么大问题,因为缓存将在下一个调用时更新(不会给出错误答案),但我想提一下。我觉得没有简单的方法避免这个问题,而不进行很多改动。
0

评论者:bronsa

亚历克斯,谢谢你来看这个问题,
1- NIL对象是方法实现缓存中nil的一个占位符,因为我们使用find-and-cache-protocol-impl测试nil?来知道分发是否已缓存。

2- 这个变化纯粹是为了一致性,使find-and-cache-protocol-impl始终返回一个类/接口,而不是类/接口或具体的实例。在行为上没有任何变化,因为find-protocol-impl的两个消费者,即find-protocol-methodsatisfies?,在这个情况下都不关心该值是什么。

3- 你是对的,这确实不是线程安全的,但我认为这是一个合理的权衡,因为它不会引起任何错误的行为,并且在最坏的情况下只会造成额外的缓存错过。使其线程安全将意味着在每个缓存命中/错过时都会有一个额外的性能惩罚。

0

由:michaelblume

(链接: ~bronsa) 我看到这个补丁导致行为上的变化

`
(defprotocol BoolProtocol
(proto-fn [this]))

(extend-protocol BoolProtocol
Object
(proto-fn [x] "Object impl")

nil
(proto-fn [x] "Nil impl"))

(proto-fn false)
`

使用Clojure master返回"Object impl",而在这个补丁中使用返回"Nil impl"。

...