guava 交集的性能陷阱

guava 交集

guava 工具类 Sets 提供了计算 2 个 set 交集的方法,如下

public static <E> SetView<E> intersection(final Set<E> set1, final Set<?> set2) {
    ......
}

一般认为 guava 源码比较优秀,其集合工具包大大方便了开发,谁能想到这个交集的方法存在极大的性能陷阱呢?

背景

在我去年的分享的一篇文章里,提到了用倒排索引来构建本地缓存

用倒排索引构建本地缓存chevron-right

最近一个需求是“未安装应用定向”,也就是广告只能投放给没有安装特定应用的用户,首先我们构建对应的倒排索引 Map<AppId, List<AdId>>,根据应用 Id,可以查询到不能投放的广告 Id

在竞价时,首先从大数据平台查询到用户已安装的应用列表,然后从全部 “未安装定向” 的应用里清除用户已安装应用,得到该用户的未安装应用,即

Set<Long> uninstalled = 
    Sets.difference(targetIndexService.<Long> keywords(UNINSTALLED), installed);

接下来,对每一个未安装的应用,查询出不能投放的广告列表,多个应用的不能投放广告计算交集,就得到该用户的不匹配广告,如下

    @Override
    public Set<Integer> unmatch(Collection<T> keywords) {
        Set<Integer> result = allUnitCache;
        for (T keyword : keywords) {
            result = Sets.intersection(result, unmatch(keyword));
            if (result.size() == 0) {
                break;
            }
        }
        return result;
    }

新功能上线后,广告竞价接口大量超时,同时 cpu 占用也开始告警,发生了严重的性能问题。经过调试后,问题定位到了 Sets.intersection() 方法

测试

为了验证这个性能问题,做一个简单的测试

执行测试用例,输出耗时为 elapse=8 8 毫秒完成 300 个 set 的交集,这性能岂不是很好?

如果在执行交集以后,获取一下交集的 size 呢?

执行测试用例,输出耗时为 elapse=207

问题就出在这里:guava 的交集操作本身很快,但是对交集返回的结果进行操作就很慢了——尤其是 size() 方法,如果改成 isEmpty() 就会发现,性能又 ok 了

原因分析

其实看下 guava 源码就很清楚了

size() 操作需要遍历整个集合,之所以要遍历整个集合,原因是 guava 的交集实际上是 set1 和 set2 的一个逻辑视图,这个遍历包括了判断 set1 的元素是否存在 set2 里

解决办法

这里提供 2 种解决办法

基于 guava

将 set 逻辑视图转换成真实的 set

自己实现交集

再回首

今天用 sonarlint 分析了项目代码,关于 size() 方法,是这样提示的

这意味着,如果只是判断集合是否为空,不应该使用 size() 方法,而应该使用 isEmpty():size() 方法的时间复杂度可能是 O(n),而 isEmpty() 的时间复杂度应该是 O(1)

Last updated