使用 Scala 时经常需要集合的相关操作,最基本的就是交集、并集偶尔也会用到差集,博主在线上场景中使用 Scala 原生 Sets 的交集 intersect、并集 union 以及差集 diff 方法发现效率比较低,于是找到了 Google Guava 库下的高效 Sets 库 com.google.common.collect.Sets,下面看下二者的使用与效率。
首先初始化两个 Sets 用于后续的测试使用:
- val random = new Random()
-
- val testSet1: mutable.Set[Int] = collection.mutable.Set.apply((0 to 10)
- .map(_ => random.nextInt(10)).toArray:_*)
- val testSet2: mutable.Set[Int] = collection.mutable.Set.apply((0 to 8)
- .map(_ => random.nextInt(8)).toArray:_*)
这里用到了 apply 和 Scala 变长参数 :_*,更多使用可以参考:Scala 变长参数之*与:_* 。
Tips:
- <dependency>
- <groupId>com.google.guavagroupId>
- <artifactId>guavaartifactId>
- <version>23.0version>
- <scope>providedscope>
- dependency>
由于 Google Guava 库基于 Java 开发,所以对应的 Set 类型为 java.util.Set,如果使用 Java 不受影响,使用 Scala 则需要使用 JavaConverters 或 JavaConvertions 对 Scala Collection 进行转换,使其类型为 java.util.Sets,多种转换方法可以参考:Wrappers$MutableSetWrapper。
- /**
- * @Describe Scala / Guava 交集对比
- * @params [arr1, arr2, format]
- * @return void
- * @author DDD
- * @since 2022/10/19 11:05 上午
- */
- def intersectionTest(set1: mutable.Set[Int], set2: mutable.Set[Int], format: String): Unit = {
- val st = System.currentTimeMillis()
- var count = 0
- while (count < epoch) {
- val re = if (format.equals("ori")) {
- set1.intersect(set2)
- } else {
- val utilSet1 = JavaConverters.mutableSetAsJavaSetConverter(set1).asJava
- val utilSet2 = JavaConverters.mutableSetAsJavaSetConverter(set2).asJava
- Sets.intersection(utilSet1, utilSet2)
- }
- if (count == 1) {
- println(s"Format: $format $re")
- }
- count += 1
- }
- val cost = System.currentTimeMillis() - st
- val log = s"Format: $format Epoch: $epoch Cost: $cost Mean: ${cost.toDouble / epoch}"
- println(log)
- }
ori 模式使用 Scala 原生 Set 交集方法 intersect,guava 使用 Sets.intersection 方法:
- // 交集测试
- intersectionTest(testSet1, testSet2, "ori")
- intersectionTest(testSet1, testSet2, "guava")
- =================================================
- Format: ori Set(1, 2, 6, 7, 4)
- Format: ori Epoch: 10000 Cost: 83 Mean: 0.0083
- Format: guava [1, 2, 6, 7, 4]
- Format: guava Epoch: 10000 Cost: 33 Mean: 0.0033
- =================================================
分别调用上述方法 10000 次保证足够大的实验样本,实践 Guava 速度是原生的 2 倍多。
- /**
- * @Describe Scala / Guava 并集对比
- * @params [arr1, arr2, format]
- * @return void
- * @author DDD
- * @since 2022/10/19 11:05 上午
- */
- def unionTest(set1: mutable.Set[Int], set2: mutable.Set[Int], format: String): Unit = {
- val st = System.currentTimeMillis()
- var count = 0
- while (count < epoch) {
- val re = if (format.equals("ori")) {
- set1.union(set2)
- } else {
- val utilSet1 = JavaConverters.mutableSetAsJavaSetConverter(set1).asJava
- val utilSet2 = JavaConverters.mutableSetAsJavaSetConverter(set2).asJava
- Sets.union(utilSet1, utilSet2)
- }
- if (count == 1) {
- println(s"Format: $format $re")
- }
- count += 1
- }
- val cost = System.currentTimeMillis() - st
- val log = s"Format: $format Epoch: $epoch Cost: $cost Mean: ${cost.toDouble / epoch}"
- println(log)
- }
同理,这里使用原生的 union 和 Sets.union 进行对比:
- // 并集测试
- unionTest(testSet1, testSet2, "ori")
- unionTest(testSet1, testSet2, "guava")
- =================================================
- Format: ori Set(0, 9, 1, 5, 2, 6, 3, 7, 4)
- Format: ori Epoch: 10000 Cost: 22 Mean: 0.0022
- Format: guava [0, 9, 1, 5, 2, 6, 7, 4, 3]
- Format: guava Epoch: 10000 Cost: 6 Mean: 6.0E-4
- =================================================
这次原生与 Guava 的差距扩大到 3 倍多。
- /**
- * @Describe Scala / Guava 并集对比
- * @params [arr1, arr2, format]
- * @return void
- * @author DDD
- * @since 2022/10/19 11:05 上午
- */
- def diffTest(set1: mutable.Set[Int], set2: mutable.Set[Int], format: String): Unit = {
- val st = System.currentTimeMillis()
- var count = 0
- while (count < epoch) {
- val re = if (format.equals("ori")) {
- set1.diff(set2)
- } else {
- val utilSet1 = JavaConverters.mutableSetAsJavaSetConverter(set1).asJava
- val utilSet2 = JavaConverters.mutableSetAsJavaSetConverter(set2).asJava
- Sets.difference(utilSet1, utilSet2)
- }
- if (count == 1) {
- println(s"Format: $format $re")
- }
- count += 1
- }
- val cost = System.currentTimeMillis() - st
- val log = s"Format: $format Epoch: $epoch Cost: $cost Mean: ${cost.toDouble / epoch}"
- println(log)
- }
相比并集、交集,差集的使用可能相对较少,下面比较下效率:
- // 差集测试
- diffTest(testSet1, testSet2, "ori")
- diffTest(testSet1, testSet2, "guava")
- =================================================
- Format: ori Set(0, 9, 5)
- Format: ori Epoch: 10000 Cost: 18 Mean: 0.0018
- Format: guava [0, 9, 5]
- Format: guava Epoch: 10000 Cost: 5 Mean: 5.0E-4
- =================================================
同样效率相差 3 倍多。
上面测试的 mutable.Sets 的 Size 大约在 10 左右,为了实验的严谨性,我们将 Sets Size 修改为 1000 左右再次测试:
- val random = new Random()
-
- val testSet1: mutable.Set[Int] = collection.mutable.Set.apply((0 to 1000)
- .map(_ => random.nextInt(1000)).toArray:_*)
- val testSet2: mutable.Set[Int] = collection.mutable.Set.apply((0 to 800)
- .map(_ => random.nextInt(800)).toArray:_*)
- =================================================
- // 交集
- Format: ori Epoch: 10000 Cost: 301 Mean: 0.0301
- Format: guava Epoch: 10000 Cost: 40 Mean: 0.004
- =================================================
- // 并集
- Format: ori Epoch: 10000 Cost: 381 Mean: 0.0381
- Format: guava Epoch: 10000 Cost: 18 Mean: 0.0018
- =================================================
- // 差集
- Format: ori Epoch: 10000 Cost: 380 Mean: 0.038
- Format: guava Epoch: 10000 Cost: 13 Mean: 0.0013
- =================================================
随着 Sets Size 的扩大,可以看到二者的效率差异也是肉眼可见的大,除此之外,由于 Scala Sets 需要转换至 java.util.Sets 同样需要时间,所以如果直接使用 java.util.Sets,运行效率会更进一步。