【问题标题】:Why is A.issuperset(B) much slower than all(b in A for b in B)?为什么 A.issuperset(B) 比 all(b in A for b in B) 慢得多?
【发布时间】:2020-11-22 09:43:33
【问题描述】:

考虑测试一个集合 A 是否是可迭代 B 的超集,一次使用该集的方法,一次使用我自己的表达式,使用超集的定义:

>>> A = set(range(1000))
>>> B = range(-1000, 0)
>>> A.issuperset(B)
False
>>> all(b in A for b in B)
False

现在让我们计时:

>>> from timeit import timeit
>>> timeit(lambda: A.issuperset(B))
52.666367300000005
>>> timeit(lambda: all(b in A for b in B))
0.9698789999999917

set 自己的方法要慢得多。为什么?大概它可以/应该做同样的事情,但是以 C 速度,所以应该更快

我正在使用 CPython 3.8.1。

【问题讨论】:

标签: python performance set python-internals


【解决方案1】:

set.issubsetset.issupersetimplementation 坚持首先建立一个论点:

static PyObject *
set_issubset(PySetObject *so, PyObject *other)
{
    setentry *entry;
    Py_ssize_t pos = 0;
    int rv;

    if (!PyAnySet_Check(other)) {
        PyObject *tmp, *result;
        tmp = make_new_set(&PySet_Type, other);
        if (tmp == NULL)
            return NULL;
        result = set_issubset(so, tmp);
        Py_DECREF(tmp);
        return result;
    }
    if (PySet_GET_SIZE(so) > PySet_GET_SIZE(other))
        Py_RETURN_FALSE;

    while (set_next(so, &pos, &entry)) {
        rv = set_contains_entry((PySetObject *)other, entry->key, entry->hash);
        if (rv < 0)
            return NULL;
        if (!rv)
            Py_RETURN_FALSE;
    }
    Py_RETURN_TRUE;
}

PyDoc_STRVAR(issubset_doc, "Report whether another set contains this set.");

static PyObject *
set_issuperset(PySetObject *so, PyObject *other)
{
    PyObject *tmp, *result;

    if (!PyAnySet_Check(other)) {
        tmp = make_new_set(&PySet_Type, other);
        if (tmp == NULL)
            return NULL;
        result = set_issuperset(so, tmp);
        Py_DECREF(tmp);
        return result;
    }
    return set_issubset((PySetObject *)other, (PyObject *)so);
}

PyDoc_STRVAR(issuperset_doc, "Report whether this set contains another set.");

issuperset 从参数中创建一个集合,然后调用other.issubset(self)。 (issubset 也坚持使用一个集合作为它的参数,但它得到了一个,所以在这种情况下不需要转换。)他们可以很容易地向issuperset 添加一个代码路径来处理非集合没有集合转换的参数,但他们没有。

我怀疑其原因可能是在调用 {1}.issuperset([2, [3]]) 时抛出错误,其中参数包含不可散列的元素。但是,也可能没有人费心对其进行优化。在Python issue tracker 中搜索issuperset 会发现关于优化issuperset 的0 个问题,甚至没有关闭的问题。令人惊讶的是,closed issueissubset 进行了困难的优化,但虽然它会导致类似的异常行为变化,但关于该问题的回复都没有说明这一点。

【讨论】:

  • 关于这个可能的原因:也许吧,尽管documentation 只是说“测试 other 中的每个元素是否都在集合中”并且 other 可以是“任何可迭代的”,我没有看到任何关于可散列元素的信息。
  • issubset 优化似乎不仅更困难而且空间效率更低,跟踪集合中已经在迭代中遇到的元素。对于issuperset,这是不必要的。
  • 我认为我们可以排除哈希性原因,因为相关的{1}.isdisjoint([1, [2]]) 不会抱怨,只会返回False
【解决方案2】:

似乎差异来自这样一个事实,即在您的测试中,您实际上并没有在两个集合之间运行issuperset,而是在一个集合和一个范围之间运行。大部分时间都花在将范围转换为集合上。考虑以下时间安排:

A = set(range(1000))
B_set = set(range(-1000, 0))
B_range = range(-1000, 0)
B_list = list(range(-1000, 0))

%%timeit 
A.issuperset(B_set)
654 ns ± 6.09 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit 
A.issuperset(B_range)
29.9 µs ± 259 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

%%timeit 
A.issuperset(B_list)
15.4 µs ± 233 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)


# Creating a set from a range. 
%%timeit
B_set = set(B_range)
29.2 µs ± 209 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

%%timeit 
all(b in A for b in B_set)
816 ns ± 16.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit 
all(b in A for b in B_range)
474 ns ± 4.74 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-06
    • 2023-04-05
    • 2014-03-29
    • 1970-01-01
    • 2010-12-08
    • 1970-01-01
    • 2018-01-29
    相关资源
    最近更新 更多