使用多核的前缀搜索算法

时间:2018-09-11 07:55:37

标签: java multithreading algorithm search multiprocessing

我有一个任务是从单词中过滤一个列表(向量)作为前缀。 该算法应该使用现代的多核处理器。

解决方案是使用许多线程来处理列表。

//      PrintWriter writer = new PrintWriter("C:\\DemoList.txt", "UTF-8");
//      
//      for(char i = 'A'; i<= 'Z'; i++) {
//          for(char j = 'A'; j<= 'Z'; j++) {
//              for(char n = 'A'; n<= 'Z'; n++) {
//                  for(char m = 'A'; m<= 'Z'; m++) {
//                      writer.println("" + i + j + n + m );
//                  }
//                      
//              }
//          }
//      }
    List<String> allLines = Files.readAllLines(Paths.get("C:\\", "DemoList.txt"));
    Collections.shuffle(allLines);
    String pattern = "AA";

    List<String> result = new ArrayList<>();
    int cores = Runtime.getRuntime().availableProcessors();
    int threadsNum = allLines.size() / cores;

    long start_time = System.nanoTime();

    for (String word : allLines) {
        if (word.startsWith(pattern))
            result.add(word);

    }

    long end_time = System.nanoTime();
    double difference = (end_time - start_time) / 1e6;
    System.out.println("Time difference in Milliseconds with Brute-Force: " + difference);

//With Parallisim:
    long new_start_time = System.nanoTime();

    List<String> filteredList = allLines.parallelStream().filter(s -> s.startsWith(pattern))
            .collect(Collectors.toList());

    long new_end_time = System.nanoTime();

    double new_difference = (new_end_time - new_start_time) / 1e6;
    System.out.println("Time difference in Milliseconds with Stream from Java 8: " + new_difference);   

结果: 蛮力时差(以毫秒为单位):33.033602 Java 8中Stream的时差(以毫秒为单位):65.017069

每个线程都应从列表中过滤一个范围。

您有更好的主意吗? 您是否认为我应该对原始列表进行排序,而不是对其进行二进制搜索?我是否还应该在二进制排序中使用多线程,还是应该使用Collections.sort? 您将如何实施?

2 个答案:

答案 0 :(得分:3)

在您的代码示例中,您的时间测量方法与Micro Benchmarking紧密相关,为此,仅测量一次执行的时间会产生误导。

您可以在以下StackOverflow帖子中详细讨论它:How do I write a correct micro-benchmark in Java?

我写了一个更准确的基准,以更准确地测量您的示例代码。该代码已在具有多线程功能的QuadCore i7上运行(但是由于功率和热量管理的原因,它是一台笔记本电脑,结果可能与产生更多热量的多线程代码略有偏差。

@Benchmark
public void testSequentialFor(Blackhole bh, Words words) {
    List<String> filtered = new ArrayList<>();
    for (String word : words.toSort) {
        if (word.startsWith(words.prefix)) {
            filtered.add(word);
        }
    }
    bh.consume(filtered);
}

@Benchmark
public void testParallelStream(Blackhole bh, Words words) {
    bh.consume(words.toSort.parallelStream()
            .filter(w -> w.startsWith(words.prefix))
            .collect(Collectors.toList())
    );
}

@Benchmark
public void testManualThreading(Blackhole bh, Words words) {
    // This is quick and dirty, bit gives a decent baseline as to
    // what a manually threaded partitionning can achieve.
    List<Future<List<String>>> async = new ArrayList<>();
    // this has to be optimized to avoid creating "almost empty" work units
    int batchSize = words.size / ForkJoinPool.commonPool().getParallelism();
    int numberOfDispatchedWords = 0;
    while (numberOfDispatchedWords < words.toSort.size()) {
        int start = numberOfDispatchedWords;
        int end = numberOfDispatchedWords + batchSize;
        async.add(words.threadPool.submit(() -> {
            List<String> batch = words.toSort.subList(start, Math.min(end, words.toSort.size()));
            List<String> result = new ArrayList<>();
            for (String word : batch) {
                if (word.startsWith(words.prefix)) {
                    result.add(word);
                }
            }
            return result;
        }));
        numberOfDispatchedWords += batchSize;
    }
    List<String> result = new ArrayList<>();
    for (Future<List<String>> asyncResult : async) {
        try {
            result.addAll(asyncResult.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    bh.consume(result);
}

@State(Scope.Benchmark)
public static class Words {

    ExecutorService threadPool = ForkJoinPool.commonPool();

    List<String> toSort;

    @Param({"100", "1000", "10000", "100000"})
    private int size;

    private String prefix = "AA";

    @Setup
    public void prepare() {
        //a 4 to 13 letters long, random word
        //for more precision, it should not be that random (use a fixed seed), but given the simple nature of the fitlering, I guess it's ok this way
        Supplier<String> wordCreator = () -> RandomStringUtils.random(4 + ThreadLocalRandom.current().nextInt(10));
        toSort = Stream.generate(wordCreator).limit(size).collect(Collectors.toList());
    }
}

这是结果

Benchmark                     (size)   Mode  Cnt        Score       Error  Units
PerfTest.testManualThreading     100  thrpt    6    95971,811 ±  1100,589  ops/s
PerfTest.testManualThreading    1000  thrpt    6    76293,983 ±  1632,959  ops/s
PerfTest.testManualThreading   10000  thrpt    6    34630,814 ±  2660,058  ops/s
PerfTest.testManualThreading  100000  thrpt    6     5956,552 ±   529,368  ops/s
PerfTest.testParallelStream      100  thrpt    6    69965,462 ±   451,418  ops/s
PerfTest.testParallelStream     1000  thrpt    6    59968,271 ±   774,859  ops/s
PerfTest.testParallelStream    10000  thrpt    6    29079,957 ±   513,244  ops/s
PerfTest.testParallelStream   100000  thrpt    6     4217,146 ±   172,781  ops/s
PerfTest.testSequentialFor       100  thrpt    6  3553930,640 ± 21142,150  ops/s
PerfTest.testSequentialFor      1000  thrpt    6   356217,937 ±  7446,137  ops/s
PerfTest.testSequentialFor     10000  thrpt    6    28894,748 ±   674,929  ops/s
PerfTest.testSequentialFor    100000  thrpt    6     1725,735 ±    13,273  ops/s

因此,顺序版本在最多几千个元素的情况下看起来行进的速度更快,它们与手动线程在10k之前的速度相当,而并行流在之后的10k相当,并且线程代码的性能优于在那里。

考虑到编写“手动线程变体”所需的代码量,以及通过计算批处理大小而在其中创建错误或效率低下的风险,即使看起来像这样,我也可能不会选择该选项比巨大列表的流更快。

我不会先排序,然后进行二进制搜索,因为过滤是O(N)操作,然后对O(Nlog(N))进行排序(必须在其上添加二进制搜索)。因此,除非您对数据有一个非常精确的模式,否则我认为它无法发挥您的优势。

请注意,尽管没有得出结论,但该基准测试无法支持。一方面,它基于这样的假设,即过滤是程序中唯一发生的事情,并且在争夺CPU时间。如果您使用的是任何类型的“多用户”应用程序(例如Web应用程序),那可能就不正确了,尽管您可能会通过多线程获得好处,但是您很可能会失去所有东西。

答案 1 :(得分:2)

从Java 8开始,您可以使用流在几行中解决问题:

List<String> yourList = new ArrayList<>(); // A list whose size requires parallelism
String yourPrefix = ""; // Your custom prefix
final List<String> filteredList = yourList.parallelStream()
               .filter(s -> s.startsWith(yourPrefix))
               .collect(Collectors.toList());

我建议您this reading,并看一下this question,以了解并行性是否会对您有所帮助。