高级Java优化

时间:2011-08-13 04:25:15

标签: java algorithm language-agnostic distributed bigdata

关于如何进行低级Java优化有很多问题和答案以及意见,包括for,while和do-while循环,以及是否有必要。

我的问题更多的是基于高级别的设计优化。我们假设我必须做以下事情:

对于给定的字符串输入,计算字符串中每个字母的出现次数。

当字符串是几个句子时,这不是一个主要问题,但如果我们想要计算900,000个单词文件中每个单词的出现,那该怎么办呢?建设循环只是浪费时间。

那么什么是可以应用于此类问题的高级设计模式。

我想我的主要观点是我倾向于使用循环来解决许多问题,而且我想摆脱使用循环的习惯。

提前致谢

萨姆

P.S。如果可能,你可以产生一些伪代码来解决900,000字文件问题,我倾向于理解代码比我能理解英语更好,我认为对于这个网站的大多数访问者来说是相同的

6 个答案:

答案 0 :(得分:10)

字数统计问题是大数据世界中涉及范围最广的问题之一;它就像Hadoop这样的框架的Hello World。您可以在整个网络上找到有关此问题的充足信息。

无论如何,我会给你一些想法。

首先,900000个单词可能仍然足够小以构建一个散列图,所以不要忽视明显的内存中方法。你说伪代码很好,所以:

h = new HashMap<String, Integer>();
for each word w picked up while tokenizing the file {
  h[w] = w in h ? h[w]++ : 1
}

现在,一旦你的数据集太大而无法构建内存中的hashmap,你就可以这样计算:

Tokenize into words writing each word to a single line in a file
Use the Unix sort command to produce the next file
Count as you traverse the sorted file

这三个步骤在Unix管道中进行。让操作系统为您完成这项工作。

现在,当您获得更多数据时,您希望引入像hadoop这样的map-reduce框架来对计算机集群进行单词计数。

现在,我听说当你进入大量数据集时,在分布式环境中做事情已经无济于事了,因为传输时间超过了计数时间,而在你的字数统计中,一切都必须“无论如何都要把它放在一起“那么你必须使用一些我怀疑你可以在研究论文中找到的非常复杂的技术。

<强>附录

OP要求提供一个用Java标记输入的示例。这是最简单的方法:

import java.util.Scanner;
public class WordGenerator {
    /**
     * Tokenizes standard input into words, writing each word to standard output,
     * on per line.  Because it reads from standard input and writes to standard
     * output, it can easily be used in a pipeline combined with sort, uniq, and
     * any other such application.
     */
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        while (input.hasNext()) {
            System.out.println(input.next().toLowerCase());
        }
    } 
}

现在这是一个使用它的例子:

echo -e "Hey Moe! Woo\nwoo woo nyuk-nyuk why soitenly. Hey." | java WordGenerator

此输出

hey
moe!
woo
woo
woo
nyuk-nyuk
why
soitenly.
hey.

您可以将此标记器与sort和uniq结合使用,如下所示:

echo -e "Hey Moe! Woo\nwoo woo nyuk-nyuk why soitenly. Hey." | java WordGenerator | sort | uniq

屈服

hey
hey.
moe!
nyuk-nyuk
soitenly.
why
woo

现在,如果您只想保留字母并丢弃所有标点符号,数字和其他字符,请将扫描仪定义行更改为:

Scanner input = new Scanner(System.in).useDelimiter(Pattern.compile("\\P{L}"));

现在

echo -e "Hey Moe! Woo\nwoo woo^nyuk-nyuk why#2soitenly. Hey." | java WordGenerator | sort | uniq

产量

hey
moe
nyuk
soitenly
why
woo

输出中有一个空行;我会告诉你如何打击它。 :)

答案 1 :(得分:3)

对此最快的解决方案是O(n)AFAIK使用循环来迭代字符串,获取字符并相应地更新HashMap中的计数。最后,HashMap包含发生的所有字符和所有出现的计数。

一些pseduo-code(可能无法编译)

HashMap<Character, Integer> map = new HashMap<Character, Integer>();
for (int i = 0; i < str.length(); i++)
{
    char c = str.charAt(i);
    if (map.containsKey(c)) map.put(c, map.get(c) + 1);
    else map.put(c, 1);
}

答案 2 :(得分:1)

你很难比使用循环来解决这个问题更好。 IMO,加速此类操作的最佳方法是将工作负载分成不同的工作单元,并使用不同的处理器处理工作单元(例如,如果您有多处理器计算机,则使用线程)。

答案 3 :(得分:1)

你不应该假设900,000是很多单词。如果你有一个8线程和3 GHZ的CPU,每秒240亿个时钟周期。 ;)

然而,使用int[]对字符进行计数会快得多。只有65,536个可能的字符。

StringBuilder words = new StringBuilder();
Random rand = new Random();
for (int i = 0; i < 10 * 1000 * 1000; i++)
    words.append(Long.toString(rand.nextLong(), 36)).append(' ');
String text = words.toString();

long start = System.nanoTime();
int[] charCount = new int[Character.MAX_VALUE];
for (int i = 0; i < text.length(); i++)
    charCount[text.charAt(i)]++;
long time = System.nanoTime() - start;
System.out.printf("Took %,d ms to count %,d characters%n", time / 1000/1000, text.length());

打印

Took 111 ms to count 139,715,647 characters

即使是单词数量的11倍,也只需要几分之一秒。

更长的并行版本要快一点。

public static void main(String... args) throws InterruptedException, ExecutionException {
    StringBuilder words = new StringBuilder();
    Random rand = new Random();
    for (int i = 0; i < 10 * 1000 * 1000; i++)
        words.append(Long.toString(rand.nextLong(), 36)).append(' ');
    final String text = words.toString();

    long start = System.nanoTime();
    // start a thread pool to generate 4 tasks to count sections of the text.
    final int nThreads = 4;
    ExecutorService es = Executors.newFixedThreadPool(nThreads);
    List<Future<int[]>> results = new ArrayList<Future<int[]>>();
    int blockSize = (text.length() + nThreads - 1) / nThreads;
    for (int i = 0; i < nThreads; i++) {
        final int min = i * blockSize;
        final int max = Math.min(min + blockSize, text.length());
        results.add(es.submit(new Callable<int[]>() {
            @Override
            public int[] call() throws Exception {
                int[] charCount = new int[Character.MAX_VALUE];
                for (int j = min; j < max; j++)
                    charCount[text.charAt(j)]++;
                return charCount;
            }
        }));
    }
    es.shutdown();
    // combine the results.
    int[] charCount = new int[Character.MAX_VALUE];
    for (Future<int[]> resultFuture : results) {
        int[] result = resultFuture.get();
        for (int i = 0, resultLength = result.length; i < resultLength; i++) {
            charCount[i] += result[i];
        }
    }
    long time = System.nanoTime() - start;
    System.out.printf("Took %,d ms to count %,d characters%n", time / 1000 / 1000, text.length());
}

打印

Took 45 ms to count 139,715,537 characters

但是对于一个不到一百万字的字符串,它不太值得。

答案 4 :(得分:0)

作为一般规则,您应该以简单的方式编写内容,然后进行性能调整以尽可能快地完成。 如果这意味着加入更快的算法,那么这样做,但首先,保持简单。 对于像这样的小程序,它不会太难。

性能调优的基本技能是不要猜测。 相反,让程序本身告诉你要修复什么。 This is my method.

对于更多参与计划like this one,经验将向您展示如何避免过度思考,最终导致其试图避免的大量不良表现。

答案 5 :(得分:0)

您必须使用分而治之方法,避免争夺资源。存在不同的方法和/或实现。这个想法是一样的 - 拆分工作并并行处理。

在单台计算机上,您可以在单独的线程中处理数据块,尽管在​​同一磁盘上放置块会大大减慢速度。 H拥有更多线程意味着有更多的上下文切换,因为吞吐量是恕我直言,更好地拥有更少的数量并让他们忙碌。

您可以将处理拆分为各个阶段并使用 SEDA 或类似内容以及您为 map-reduce 所做的非常大的数据 - 只需计算分配数据的费用跨集群。

我很高兴有人指出另一个广泛使用的API。