【问题标题】:Multithreaded string processing blows up with #threads多线程字符串处理因#threads 而爆炸
【发布时间】:2015-04-14 20:09:12
【问题描述】:

我正在处理一个多线程项目,我们必须将文件中的一些文本解析为一个魔术对象,对对象进行一些处理,然后汇总输出。旧版本的代码在一个线程中解析文本,并使用 Java 的ExecutorService 在线程池中进行对象处理。我们没有得到我们想要的性能提升,而且相对于每个对象的处理时间,解析所花费的时间比我们想象的要长,因此我尝试将解析移到工作线程中。

这应该可行,但实际发生的是每个对象的时间作为池中线程数的函数而爆炸。它比线性差,但不如指数差。

我已将其缩减为一个小示例,该示例(无论如何在我的机器上)显示了该行为。该示例甚至没有创建魔术对象;它只是在进行字符串操作。我看不到线程间的依赖关系;我知道split() 效率不是很高,但我无法想象为什么它会在多线程上下文中甩掉床。我错过了什么吗?

我在 24 核机器上运行 Java 7。行很长,每行约 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&lt;String&gt;。然后我遍历列表以创建池的作业。这使得 substrings-eating-the-heap 假设不太可能,不是吗?
  • 编译一份供所有工作线程使用的分隔符模式没有任何效果。 :(

分辨率:

我已将解析代码转换为使用基于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;       
}

奇怪的是,随着线程数量的增加,不会爆炸,我不知道为什么。不过,这是一个实用的解决方法,所以我会接受它......

【问题讨论】:

  • pool.awaitTermination(7, TimeUnit.DAYS); - 希望这就足够了。
  • 在我开始深入研究之前只是一个快速的想法:您是否尝试过先将整个文件(或其中的一大块)读入内存,然后尝试解析?
  • 只是一种预感,但我会研究String#split(...) 的实现,看看它在有大量元素时的执行情况。例如,它可能假设元素数量较少,并且必须多次重新分配结果数组。
  • 您是否尝试过分析以确定什么是真正的慢? BufferedReader(以及扩展名LineNumberReader)必须从输入中一一读取每个字符,直到找到行终止符。如果这是减速,我想知道Scanner 是否会提供更好的整体性能。
  • 可以改一下格式吗?

标签: java string parsing executorservice


【解决方案1】:

在 Java 7 中,String.split() 在内部使用 String.subString(),出于“优化”的原因,它不会创建真正的新 Strings,而是空的 String 外壳,指向原始外壳的子部分。

因此,当您将 split()String 分成小块时,原始的(可能很大)仍在内存中,最终可能会吃掉所有的堆。我看到你解析大文件,这可能是一个风险(这在 Java 8 中已更改)。

鉴于您的格式众所周知,我建议“手动”解析每一行,而不是使用 String.split()(反正正则表达式对性能非常不利),并为子部分创建真正的新行。

【讨论】:

  • 如果可能的话,我会完全避免创建字符串。如果格式只允许基本的 ASCII 字符(从示例中看起来是这样),您可以直接使用字节。
  • > 在 Java 7 中,String.split() 在内部使用 String.subString(),出于“优化”的原因,它不会创建真正的新字符串——看起来它只对早期版本有效java 7:java-performance.info/changes-to-string-java-1-7-0_06?
【解决方案2】:

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 7 中的(至少)Oracle JRE 在内部使用indexOf 实现了这一点和迭代而不是正则表达式。
猜你喜欢
  • 2011-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多