【问题标题】:Clojure Set vs Map Lookup Performance DifferencesClojure Set 与 Map Lookup 性能差异
【发布时间】:2019-11-18 09:27:10
【问题描述】:

我有一个 uid 列表并想检查一个 uid 是否是该列表的成员

实现它的自然方法是创建一组 (clojure.set) uid 并在该列表中搜索该成员

我发现映射键查找要快得多 - 我使用以下 sn-p 对这两种方法进行基准测试:

(def uids #{:a :b :c :d :e :f :g :h :i :j :k :l :m :n :o :p :a1 :b1 :c1 :d1 :e1 :f1 :h1 :i1 :j1 :k1 :l1 :m1 :n1 :o1 :p1})
(def uids-map (reduce (fn [acc v] (assoc acc v true)) {} uids))
(time (dotimes [i 1000000] (:o1 uids)))
;user=> "Elapsed time: 191.076266 msecs"
(time (dotimes [i 1000000] (:o1 uids-map)))
;user=> "Elapsed time: 38.159388 msecs"

结果在调用之间非常一致 - 地图查找大约占集合查找的 1/5

那么设置不是最佳的键查找还是我使用错误的方式?

此外,这些基准测试存在差异的原因是什么?

我的印象是在 clojure 中将集合实现为类似于向量的关联数据结构 - 那么为什么键查找比简单的映射要慢得多?

【问题讨论】:

  • 你试过contains?如果你只对“key”的存在感兴趣,那将是合适的功能;你在这里基准get。此外,我会为此类“微基准”使用标准
  • contains? 似乎更快,但仍然只有键查找的一半左右
  • 我有 criterium: contains/map: 16, contains/set: 34, get/map: 15, get/set: 103
  • 另外,区别不是数量级我不会花太多时间担心这个并使用最有意义的数据结构。先测量,后优化。
  • 尝试使用集合作为函数:(uids :o1) 应该和(uids-map :o1) 一样快。

标签: clojure


【解决方案1】:

我从未进入 clojure 的源代码,但从我看到的 set 实现 actually uses a map inside:

protected APersistentSet(IPersistentMap impl){
    this.impl = impl;
}

它还将invoke 调用委托给内部映射。

APersistentSet:

public Object invoke(Object arg1) {
    return get(arg1);
}

// ....

public Object get(Object key){
    return impl.valAt(key);
}

APersistentMap:

public Object invoke(Object arg1) {
    return valAt(arg1);
}

public Object invoke(Object arg1, Object notFound) {
    return valAt(arg1, notFound);
}

所以这不能解释差异。

作为@cgrand 的mentioned in the comments,当我们反转参数时它更快(并且大致相同,因为我们立即调用set 的invoke)。所以我查了Keywordinvoke,这可能是(:k obj)使用的:

final public Object invoke(Object obj, Object notFound) {
    if(obj instanceof ILookup)
        return ((ILookup)obj).valAt(this,notFound);
    return RT.get(obj, this, notFound);
}

需要注意的重要一点是ILookup 是在APersistentMap(通过Associative)中实现的,而不是在APersistentSet 中实现的。您也可以在 clojure 中进行验证:

(instance? clojure.lang.ILookup #{}) ;; false
(instance? clojure.lang.ILookup {})  ;; true

所以地图通过“快乐路径”并最终设置在RT.get,我认为这是运行时。

让我们看看运行时。

它最初尝试做与关键字几乎相同的事情:

static public Object get(Object coll, Object key){
    if(coll instanceof ILookup)
        return ((ILookup) coll).valAt(key);
    return getFrom(coll, key);
}

但由于我们知道集合不实现ILookup,我们知道它们会转到RT.getFrom

static Object getFrom(Object coll, Object key){
    if(coll == null)
        return null;
    else if(coll instanceof Map) {
        Map m = (Map) coll;
        return m.get(key);
    }
    else if(coll instanceof IPersistentSet) {
        IPersistentSet set = (IPersistentSet) coll;
        return set.get(key);
    }
    else if(key instanceof Number && (coll instanceof String || coll.getClass().isArray())) {
        int n = ((Number) key).intValue();
        if(n >= 0 && n < count(coll))
            return nth(coll, n);
        return null;
    }
    else if(coll instanceof ITransientSet) {
        ITransientSet set = (ITransientSet) coll;
        return set.get(key);
    }

    return null;
}

这让我相信主要区别在于额外的委托和 instanceof 调用,因为集合没有实现 ILookup

作为奖励,我在集合上添加了一个测试,该集合实现了ILookup,并立即将valAt 委托给内部映射(使用proxy),这稍微缩小了差距:

(def uids #{:a :b :c :d :e :f :g :h :i :j :k :l :m :n :o :p :a1 :b1 :c1 :d1 :e1 :f1 :h1 :i1 :j1 :k1 :l1 :m1 :n1 :o1 :p1})
(def uids-map (into {} (for [k uids] [k k])))
(def lookupable-set (proxy [clojure.lang.APersistentSet clojure.lang.ILookup] [uids-map]
                      (valAt [k] (get uids-map k))))

;; verify
(instance? clojure.lang.APersistentSet lookupable-set) ;; true
(instance? clojure.lang.ILookup lookupable-set) ;; true

(time (dotimes [i 1000000] (:o1 uids))) ;; 134.703101 msecs
(time (dotimes [i 1000000] (:o1 lookupable-set))) ;; 63.187353 msecs  <-- faster
(time (dotimes [i 1000000] (:o1 uids-map))) ;; 35.802762 msecs <-- still fastest

总结:性能很重要 - 调用集合 (#{...} k) 而不通过关键字 (k #{...}) 与 map 一样快。

但我可能是错的:)

【讨论】:

    【解决方案2】:

    contains? 的实现使用了clojure.lang.RT.contains,它有很多instanceof 检查(与containsKey 相比),这可能是导致性能差异的原因。

    【讨论】:

      猜你喜欢
      • 2021-02-03
      • 1970-01-01
      • 1970-01-01
      • 2011-01-21
      • 1970-01-01
      • 1970-01-01
      • 2020-10-15
      • 2011-12-17
      • 1970-01-01
      相关资源
      最近更新 更多