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

欢迎!请在 关于 页面查看更多有关此操作的信息。

+13
协议
目前 `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 80%-90% 的时间都花在 satisfies? 调用上了。

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$loading5561auto__4969.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$fn5618.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
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$fn5669.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$fn5618.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.core.match$eval4949.invokeStatic(form-init2494799382238714928.clj:1)
|| clojure.core.match$eval4949.invoke(form-init2494799382238714928.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_forward7404$fn7407.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_forward7404.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$fn7413.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$fn637.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 bindingsSTAR.invokeStatic
|| clojure.core$with bindingsSTAR.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$fn679$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)

`

在我自己的项目中,与clojure.core.match相关的命名空间重新加载时的行为也相同。

0
by

评论由:bronsa 添加

是涉及AOT(AOT:Ahead-of-Time编译)的可能性吗?

0

评论者为:michaelblume

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

||java.lang.ClassCastException: java.lang.Class cannot be cast to clojure.asm.Type, compiling:(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:/...|| clojure.core$require.invokeStatic zipfile:/...|| 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)|| clojure.core$eval.invokeStatic zipfile:/...|| clojure.core$eval.invoke(core.clj)|| clojure.main$repl$fn__7404$fn__7407.invoke zipfile:/...|| clojure.main$repl$fn__7413.invoke zipfile:/...|| clojure.main$repl.invokeStatic zipfile:/...|| clojure.main$repl.doInvoke(main.clj)|| clojure.tools.nrepl.middleware.interruptible_eval$evaluate$fn__637.invoke zipfile:/...|| clojure.lang.AFn.applyToHelper(AFn.java:152)|| clojure.lang.AFn.applyTo(AFn.java:144)|| clojure.core$apply.invokeStatic zipfile:/...|| clojure.core$with_bindings_STAR_.invokeStatic zipfile:/...|| clojure.tools.nrepl.middleware.interruptible_eval$evaluate.invokeStatic zipfile:/...|| clojure.tools.nrepl.middleware.interruptible_eval$evaluate.invoke(interruptible_eval.clj)|| clojure.tools.nrepl.middleware.interruptible_eval$interruptible_eval$fn__679$fn__682.invoke zipfile:/...|| clojure.tools.nrepl.middleware.interruptible_eval$run_next$fn__674.invoke zipfile:/...|| 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?}}谓词中使用,所以提高优先级 - see 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

user=> (let [x (x.)] (bench (satisfies? p x)))
评估次数:604,961,580 在60次样本中,每次样本大小为100,826,93个调用。
             平均执行时间: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




user=> (let [y (y.)] (bench (satisfies? p y)))
评估次数:22,676,100 在60次样本中,每次样本大小为377,935个调用。
             平均执行时间: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

user=> (let [x (x.)] (bench (satisfies? p x)))
评估次数:731,759,100 在60次样本中,每次样本大小为12,195,985个调用。
             平均执行时间:79.321426纳秒
    执行时间标准差:3.959245纳秒
   执行时间下四分位数:75.365187纳秒(2.5%)
   执行时间上四分位数:87.986479纳秒(97.5%)
                   开销使用:1.905711纳秒

在60个样本中找到了1个异常值(1.6667%)
    低严重度     1 (1.6667%)
 方差来自异常值:35.2614% 异常值导致方差中度膨胀
nil



user=> (let [y (y.)] (bench (satisfies? p y)))
评估次数:771,220,140 在60次样本中,每次样本大小为12,853,669个调用。
             平均执行时间: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

评论者为: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))从我的角度看并不是线程安全的 - 我认为在两个不同线程中对不同实现同时发生遗漏会导致缓存中只留下一个。这可能不太可能发生,而且可能不是一个大问题,因为缓存将在下一次调用时更新(不会给出错误答案),但想提一下。我不认为有简单的方法可以避免它而不进行很多更改。
0
by

评论由:bronsa 添加

Alex,感谢您查看此事,
1- NIL对象是方法实施缓存中nil的占位符,因为find-and-cache-protocol-impl测试nil?以确定是否已缓存分派

2- 该更改纯粹是为了保持一致性,使find-and-cache-protocol-impl始终返回一个类/接口,而不是类/接口或具体实例。从行为上讲,它不会改变任何事情,因为find-protocol-impl的两个消费者,即find-protocol-methodsatisfies?在那种情况下都不关心那个值是什么

3- 是的,你是对的,它不是线程安全的,但我觉得这是一个不错的权衡,因为它不会引起任何不正确的行为,在最差的情况下会导致额外的缓存错过,而让它在每次缓存命中/错过中都有额外的性能惩罚

0
by

评论者为: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”

...