多线程字符串处理因#threads 而爆炸
Multithreaded string processing blows up with #threads
我正在处理一个多线程项目,我们必须将文件中的一些文本解析为一个魔法对象,对该对象进行一些处理,然后聚合输出。旧版本的代码在一个线程中解析文本,并使用 Java 的 ExecutorService
在线程池中进行对象处理。我们没有获得我们想要的性能提升,事实证明,相对于每个对象的处理时间,解析花费的时间比我们想象的要长,所以我尝试将解析移到工作线程中。
这应该有效,但实际发生的是 每个对象的时间作为池中线程数的函数而爆炸 。它比线性差,但不如指数差。
我已将其缩减为一个小示例(无论如何在我的机器上)显示了该行为。该示例甚至没有创建魔法对象;它只是在进行字符串操作。我看不到线程间的依赖关系;我知道 split()
效率不是很高,但我无法想象为什么它会在多线程上下文中拉屎。我错过了什么吗?
我 运行 在 Java 7 24 核机器上。行很长,每行约 1MB。 features
中可以有几十个项目,edges
中可以有100k+个项目。
示例输入:
1 1 156 24 230 1350 id(foo):id(bar):w(house,pos):w(house,neg) 1->2:1@1.0 16->121:2@1.0,3@0.5
运行 16 个工作线程的示例命令行:
$ java -Xmx10G Foo 16 myfile.txt
示例代码:
public class Foo implements Runnable {
String line;
int id;
public Foo(String line, int id) {
this.line = line;
this.id = id;
}
public void run() {
System.out.println(System.currentTimeMillis()+" Job start "+this.id);
// line format: tab delimited
// x[4]
// graph[2]
// features[m] <-- ':' delimited
// edges[n]
String[] x = this.line.split("\t",5);
String[] graph = x[4].split("\t",4);
String[] features = graph[2].split(":");
String[] edges = graph[3].split("\t");
for (String e : edges) {
String[] ee = e.split(":",2);
ee[0].split("->",2);
for (String f : ee[1].split(",")) {
f.split("@",2);
}
}
System.out.println(System.currentTimeMillis()+" Job done "+this.id);
}
public static void main(String[] args) throws IOException,InterruptedException {
System.err.println("Reading from "+args[1]+" in "+args[0]+" threads...");
LineNumberReader reader = new LineNumberReader(new FileReader(args[1]));
ExecutorService pool = Executors.newFixedThreadPool(Integer.parseInt(args[0]));
for(String line; (line=reader.readLine()) != null;) {
pool.submit(new Foo(line, reader.getLineNumber()));
}
pool.shutdown();
pool.awaitTermination(7,TimeUnit.DAYS);
}
}
更新:
- 先将整个文件读入内存没有效果。更具体地说,我阅读了整个文件,将每一行添加到
ArrayList<String>
。然后我遍历列表以创建池的作业。这使得子串吃堆假设不太可能,不是吗?
- 编译一份分隔符模式副本以供所有工作线程使用无效。 :(
解析:
我已经将解析代码转换为使用基于 indexOf()
的自定义拆分例程,如下所示:
private String[] split(String string, char delim) {
if (string.length() == 0) return new String[0];
int nitems=1;
for (int i=0; i<string.length(); i++) {
if (string.charAt(i) == delim) nitems++;
}
String[] items = new String[nitems];
int last=0;
for (int next=last,i=0; i<items.length && next!=-1; last=next+1,i++) {
next=string.indexOf(delim,last);
items[i]=next<0?string.substring(last):string.substring(last,next);
}
return items;
}
奇怪的是,随着线程数量的增加,这 不会 爆炸,我不知道为什么。不过这是一个功能性的解决方法,所以我会接受它...
在 Java 7 中,String.split()
在内部使用 String.subString()
,出于 "optimization" 的原因,它不会创建真正的新 Strings
,而是空 [=13] =] 指向原始部分的子部分的外壳。
因此,当您 split()
将 String
分成小块时,原始的(可能很大)仍在内存中,最终可能会吃掉所有堆。我看到你解析大文件,这可能是一个风险(这已在 Java 8 中更改)。
鉴于您的格式众所周知,我建议解析每一行 "by hand" 而不是使用 String.split()
(正则表达式对性能来说真的很糟糕),并为 sub 创建真正的新格式-零件。
String.split其实就是用正则表达式来拆分,见:http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/String.java#String.split%28java.lang.String%2Cint%29
这意味着您每次迭代都在编译一大堆正则表达式。
最好为整行编译一次模式,然后在读取时将其应用于每一行以解析它们。或者,编写您自己的解析器来查找字符中断而不是正则表达式。
我正在处理一个多线程项目,我们必须将文件中的一些文本解析为一个魔法对象,对该对象进行一些处理,然后聚合输出。旧版本的代码在一个线程中解析文本,并使用 Java 的 ExecutorService
在线程池中进行对象处理。我们没有获得我们想要的性能提升,事实证明,相对于每个对象的处理时间,解析花费的时间比我们想象的要长,所以我尝试将解析移到工作线程中。
这应该有效,但实际发生的是 每个对象的时间作为池中线程数的函数而爆炸 。它比线性差,但不如指数差。
我已将其缩减为一个小示例(无论如何在我的机器上)显示了该行为。该示例甚至没有创建魔法对象;它只是在进行字符串操作。我看不到线程间的依赖关系;我知道 split()
效率不是很高,但我无法想象为什么它会在多线程上下文中拉屎。我错过了什么吗?
我 运行 在 Java 7 24 核机器上。行很长,每行约 1MB。 features
中可以有几十个项目,edges
中可以有100k+个项目。
示例输入:
1 1 156 24 230 1350 id(foo):id(bar):w(house,pos):w(house,neg) 1->2:1@1.0 16->121:2@1.0,3@0.5
运行 16 个工作线程的示例命令行:
$ java -Xmx10G Foo 16 myfile.txt
示例代码:
public class Foo implements Runnable {
String line;
int id;
public Foo(String line, int id) {
this.line = line;
this.id = id;
}
public void run() {
System.out.println(System.currentTimeMillis()+" Job start "+this.id);
// line format: tab delimited
// x[4]
// graph[2]
// features[m] <-- ':' delimited
// edges[n]
String[] x = this.line.split("\t",5);
String[] graph = x[4].split("\t",4);
String[] features = graph[2].split(":");
String[] edges = graph[3].split("\t");
for (String e : edges) {
String[] ee = e.split(":",2);
ee[0].split("->",2);
for (String f : ee[1].split(",")) {
f.split("@",2);
}
}
System.out.println(System.currentTimeMillis()+" Job done "+this.id);
}
public static void main(String[] args) throws IOException,InterruptedException {
System.err.println("Reading from "+args[1]+" in "+args[0]+" threads...");
LineNumberReader reader = new LineNumberReader(new FileReader(args[1]));
ExecutorService pool = Executors.newFixedThreadPool(Integer.parseInt(args[0]));
for(String line; (line=reader.readLine()) != null;) {
pool.submit(new Foo(line, reader.getLineNumber()));
}
pool.shutdown();
pool.awaitTermination(7,TimeUnit.DAYS);
}
}
更新:
- 先将整个文件读入内存没有效果。更具体地说,我阅读了整个文件,将每一行添加到
ArrayList<String>
。然后我遍历列表以创建池的作业。这使得子串吃堆假设不太可能,不是吗? - 编译一份分隔符模式副本以供所有工作线程使用无效。 :(
解析:
我已经将解析代码转换为使用基于 indexOf()
的自定义拆分例程,如下所示:
private String[] split(String string, char delim) {
if (string.length() == 0) return new String[0];
int nitems=1;
for (int i=0; i<string.length(); i++) {
if (string.charAt(i) == delim) nitems++;
}
String[] items = new String[nitems];
int last=0;
for (int next=last,i=0; i<items.length && next!=-1; last=next+1,i++) {
next=string.indexOf(delim,last);
items[i]=next<0?string.substring(last):string.substring(last,next);
}
return items;
}
奇怪的是,随着线程数量的增加,这 不会 爆炸,我不知道为什么。不过这是一个功能性的解决方法,所以我会接受它...
在 Java 7 中,String.split()
在内部使用 String.subString()
,出于 "optimization" 的原因,它不会创建真正的新 Strings
,而是空 [=13] =] 指向原始部分的子部分的外壳。
因此,当您 split()
将 String
分成小块时,原始的(可能很大)仍在内存中,最终可能会吃掉所有堆。我看到你解析大文件,这可能是一个风险(这已在 Java 8 中更改)。
鉴于您的格式众所周知,我建议解析每一行 "by hand" 而不是使用 String.split()
(正则表达式对性能来说真的很糟糕),并为 sub 创建真正的新格式-零件。
String.split其实就是用正则表达式来拆分,见:http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/String.java#String.split%28java.lang.String%2Cint%29 这意味着您每次迭代都在编译一大堆正则表达式。
最好为整行编译一次模式,然后在读取时将其应用于每一行以解析它们。或者,编写您自己的解析器来查找字符中断而不是正则表达式。