java8 并行字符串统计/流/map/reduce

java8 parallel string statistics / stream / map/reduce

我在 java 中有一个很长的字符串,我正在尝试获取该字符串的统计信息。

例如 String s = "afafaf"

我想获取所有长度为 2 的现有子字符串的所有计数。 对于上面的这个小例子,这将是:

"af" - 3 "fa" - 2

另一个例子: 字符串 s = "hsdjs"

结果: "hs" - 1 "sd" - 1 "dj" - 1 "js" - 1

我所做的和正在工作的是使用 for (int i=0; i < s.length;i++) 遍历字符串并迭代 Map 条目。

问题是那太慢了。 我在想也许用于并行处理的新 Java8 函数可能对我有帮助。但不幸的是我无法得到一些东西 运行...也许有人可以帮助我。

当前代码:

    import com.google.common.collect.HashMultiset;


    String inputString = s;
    HashMultiset<String> multi = HashMultiset.create();

    for (int i=0;i <inputString.length()-1;i++) {             
        String aktuellerString = inputString.substring(i, i+2);
        multi.add(aktuellerString);       
    }   

这是当前的分析: http://fs5.directupload.net/images/160909/naadsfxi.png

google guava 库的 HashMultiset 的 add() 方法实际上花费了大部分时间。但这是我能找到的最快的 collection。 (尝试了其他几个优化的库,包括普通的 HashMap、Tie、GS Collections、gnu.trove.map.hash.THashMap; 导入 org.apache.commons.collections.FastHashMap, ...)。

这就是为什么我认为并行处理可能是加速的唯一方法。

更新:正如 Marko 指出的那样,创建子字符串的成本很高,即使有多个 CPU,您也可以通过避免创建子字符串的结构来做得更好。在这种情况下,我们只有两个字符,可以将它们编码为 int 值。在这种情况下,我们可以假定 ASCII 字符。

public static void main(String[] args) throws IOException {
    char[] chars = new char[1000000000];
    Random rand = new Random();
    for (int i = 0; i < chars.length; i++)
        chars[i] = (char) (rand.nextInt(26) + 'a');
    String s = new String(chars);

    long start = System.currentTimeMillis();
    Map<String, Integer> freq = IntStream.range(0, s.length() - 1).parallel()
            .mapToObj(i -> s.substring(i, i + 2))
            .collect(Collectors.groupingBy(w -> w, Collectors.summingInt(e -> 1)));
    long time = System.currentTimeMillis() - start;
    System.out.println("Took " + time + " ms " + freq);
}

打印

Took 8401 ms {aa=1479201, ab=1478451, ac=1479055, ...

但是,如果我们直接使用 collect,我们可以使用不创建任何对象的结构。

public static void main(String[] args) throws IOException {
    char[] chars = new char[1000000000];
    Random rand = new Random();
    for (int i = 0; i < chars.length; i++)
        chars[i] = (char) (rand.nextInt(26) + 'a');
    String s = new String(chars);

    long start = System.currentTimeMillis();
    int[] freqArr = IntStream.range(0, s.length() - 1).parallel()
            .collect(() -> new int[128 * 128],
                    (arr, i) -> arr[s.charAt(i) * 128 + s.charAt(i + 1)]++,
                    (a, b) -> sum(a, b));
    Map<String, Integer> freq = new TreeMap<>();
    for (int i = 0; i < freqArr.length; i++) {
        int c = freqArr[i];
        if (c == 0) continue;
        String key = "" + (char) (i >> 7) + (char) (i & 0x7f);
        freq.put(key, c);
    }
    long time = System.currentTimeMillis() - start;
    System.out.println("Took " + time + " ms " + freq);
}

static int[] sum(int[] a, int[] b) {
    for (int i = 0; i < a.length; i++)
        a[i] += b[i];
    return a;
}

打印以下内容,速度快约 20 倍。

Took 404 ms {aa=1479575, ab=1480511, ac=1476255, 

这有很大的不同,因为我们处理的是小字符串


你可以替换

for (int i=0; i < s.length;i++) { something(i) }

IntStream.range(0, s.length()).parallel().forEach(i -> { something(i) })

但更好的解决方案是使用映射...

String s = "afafaffafafafffafaaaf";

Map<String, Long> freq = IntStream.range(0, s.length()-1).parallel() // 1
        .mapToObj(i -> s.substring(i, i + 2)) // 2
        .collect(Collectors.groupingBy(w -> w, Collectors.counting())); //3

System.out.println(freq);
  1. 遍历所有将有两个字符的索引 字符串,并联。
  2. 获取这些索引的两个字符串中的每一个。
  3. 并发按名称分组,统计所有出现的次数。

打印

{ff=3, aa=2, af=8, fa=7}

关于 Holger 关于 groupingByConcurrent 可能更慢的观点,我测试了四种情况。

    long start = System.currentTimeMillis();
    Map<Integer, Long> freq = IntStream.range(0,1000000000)/*.parralel()*/
            .mapToObj(i -> i % 10)
            .collect(Collectors.groupingBy/*Concurrent*/(w -> w, Collectors.counting()));
    long time = System.currentTimeMillis() - start;
    System.out.println("Took " + time+" ms " + freq);

without parallel(), with groupingBy : Took 14156 ms {0=100000000, 1=100000000, 2=100000000, 3=100000000, 4=100000000, 5=100000000, 6=100000000, 7=100000000, 8=100000000, 9=100000000}
with parallel(), with groupingBy : Took 5581 ms {0=100000000, 1=100000000, 2=100000000, 3=100000000, 4=100000000, 5=100000000, 6=100000000, 7=100000000, 8=100000000, 9=100000000}
without parallel(), with groupingByConcurrent : Took 38218 ms {0=100000000, 1=100000000, 2=100000000, 3=100000000, 4=100000000, 5=100000000, 6=100000000, 7=100000000, 8=100000000, 9=100000000}
with parallel(), with groupingByConcurrent : Took 27619 ms {0=100000000, 1=100000000, 2=100000000, 3=100000000, 4=100000000, 5=100000000, 6=100000000, 7=100000000, 8=100000000, 9=100000000}

使用 groupingBy 是最好的解决方案,无论是否并行。

进一步使用 Holger 的评论,通过使用 summingInt 这再次证明速度更快。

long start = System.currentTimeMillis();
Map<Integer, Integer> freq = IntStream.range(0, 1000000000).parallel()
        .mapToObj(i -> i % 10)
        .collect(Collectors.groupingBy(w -> w, Collectors.summingInt(e -> 1)));
long time = System.currentTimeMillis() - start;
System.out.println("Took " + time+" ms " + freq);

打印

Took 4131 ms {0=100000000, 1=100000000, 2=100000000, 3=100000000, 4=100000000, 5=100000000, 6=100000000, 7=100000000, 8=100000000, 9=100000000}