java - 使用多核的前缀搜索算法
问题描述
我的任务是从单词中过滤列表(向量)作为前缀。该算法应该使用现代多核处理器。
解决方案是使用许多线程来处理列表。
// 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 流的毫秒时间差:65.017069
每个线程都应该从列表中过滤一个范围。
你有更好的主意吗?您认为我应该对原始列表进行排序而不是对其进行二进制搜索吗?我应该在二进制排序中也使用多线程,还是应该使用 Collections.sort?你将如何实现它?
解决方案
从您的代码示例中,您的时间测量方法与 Micro Benchmarking 接近,因此简单地测量单次执行的时间会产生误导。
您可以在以下 StackOverflow 帖子中详细讨论它:如何在 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());
}
}
这是结果
基准(大小)模式 Cnt 分数 误差单位 PerfTest.testManualThreading 100 thrpt 6 95971,811 ± 1100,589 操作/秒 PerfTest.testManualThreading 1000 thrpt 6 76293,983 ± 1632,959 操作/秒 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 操作/秒 PerfTest.testParallelStream 1000 thrpt 6 59968,271 ± 774,859 操作/秒 PerfTest.testParallelStream 10000 thrpt 6 29079,957 ± 513,244 操作/秒 PerfTest.testParallelStream 100000 thrpt 6 4217,146 ± 172,781 操作/秒 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 应用程序)中,那么这可能不是真的,您很可能会失去一切,尽管您可以通过多线程获得。
推荐阅读
- r - Azure HDInsight 群集中的 HDFS 路径
- javascript - 从另一个文件向命名空间/模块添加函数
- android - 如何在 ARCore 中的另一个门户场景中实现门户场景?
- c - 请问有人能解释一下这个递归代码吗?
- python - python中维基百科页面的内链和外链
- blur - 找不到 UIModalPresentationStyle 枚举值 blurOverFullScreen
- tags - Gitlab 项目标签和 Runner 标签;他们有关系吗?
- compiler-errors - Gnuplot - 误差线
- java - 引起:java.lang.NumberFormatException:对于输入字符串:“?” 对于 cron 工作
- django - 使用 pk 以外的属性识别 Django Rest Framework ModelSerializer ForeignKey