高级Java优化

6
有关如何进行低级Java优化的问题,有很多问题和答案以及意见,包括使用for、while和do-while循环,以及是否必要进行优化。
我的问题更多是基于高级设计的优化。假设我必须执行以下操作:对于给定的字符串输入,计算该字符串中每个字母的出现次数。
当字符串只有几句话时,这不是一个大问题,但是如果我们想要计算900,000个单词文件中每个单词的出现次数,那么构建循环就会浪费时间。
那么,可以应用于这种类型问题的高级设计模式是什么呢?
我的主要观点是我倾向于使用循环来解决许多问题,我想改掉使用循环的习惯。
谢谢您提前的帮助!
Sam
附:如果可能,请为解决900,000个单词文件的问题提供一些伪代码,我往往比理解英语更理解代码,我认为对于本站的大多数访问者也是如此。

唯一可能的解决方案是递归,但由于Java没有为递归实现任何优化,因此会导致堆栈溢出错误,所以唯一的解决方案是循环。不确定您为什么认为循环浪费时间。 - Maurício Linhares
当涉及到优化时,循环本身并没有问题。问题在于你设计循环的质量不佳或者你关注代码的可维护性和可读性。 - Gabriel Ščerbák
通过避免循环,OP 的意思是使用内置循环的操作,比如 map、filter 和 reduce,或者编写熟练的 Unix 管道,使用 awk、cut、perl -le、sort、uniq 等工具。 - Ray Toal
@Gabriel,我认为进行100次循环操作是安全且快速的,但是当你考虑到100万次循环时,情况就有些不同了。Sam - Sam Mohamed
@Gabriel 什么是设计不良的循环? - Sam Mohamed
@Sam,循环体执行多少次并不重要,只要它有用,所以1,000、1,000,000甚至可能是无限的都可以。我所说的“设计不良”的循环,例如,如果你两次遍历文件——一次进行标记化,第二次计算单词数——结果是正确的,但你不必要地遍历了文件两次,而不是像答案中建议的那样进行单次遍历。 - Gabriel Ščerbák
6个回答

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
}

如果你的数据集太大无法建立内存哈希表,可以像下面这样进行计数:

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框架,以便在机器集群上进行单词计数。

听说当处理的数据集过于庞大时,在分布式环境下执行操作并不能起到很大帮助,因为传输时间会超过计数时间,对于单词计数这种情况,所有内容最终仍然必须“重新组合在一起”,所以您必须使用一些非常复杂的技术,我猜在研究论文中可能可以找到。

补充说明:

问题提出者要求在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

输出中有一个空白行;我会让你想办法去掉它。 :)


@Ray 很棒的答案。所以我猜对于大文件来说,纯Java解决方案不是答案。虽然你的解决方案非常依赖平台,主要是Unix。想法是要有一个跨平台的设计,对吧?我对这个在面试代码测试中很担心。想法是将一个写得很差的文件读取程序进行改进。我记得一年前我参加了亚马逊的考试,但没有通过,其中一个问题就是单词计数问题。 - Sam Mohamed
@Ray 对于内存中的方法,你能提供一些用于文件分词的Java代码吗? - Sam Mohamed
1
@Ray,你可以使用缓冲区读取文件,并将HashMap轻松存储到Java文件中。我想说,*nix解决方案与这个关于Java的问题完全无关。 - Gabriel Ščerbák
@Gabriel 有很好的观点,+1 对这个观察。你可以在Java中实现*nix排序;毕竟,它是一个具有良好已知实现的经典外部排序过程。但是如果这是一个面试问题或作业,提问者希望你从自己编写程序跳到利用环境时,他们确实很喜欢。至少我个人认为是这样的。 :) - Ray Toal
@Sam,我添加了一个完整的Java应用程序来将其标记化为答案。希望它适用于您。( Pattern类是从 java.util.regex中获取的 )。 - Ray Toal

3
这个问题的最快解决方案是O(n),可以使用循环迭代字符串,获取字符并相应地在HashMap中更新计数。最后,HashMap包含所有出现的字符及其出现次数。
以下是一些伪代码(可能无法编译):
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);
}

你需要在最后一行使用 map.put(c,1) - Ray Toal
@Ray,我发布后意识到这个问题并修复了它,你可以看到。 - Jesus Ramos
我现在明白了。时机不对。已撤回。 :) - Ray Toal
@Ray,如果我没有注意到的话,那就没事了 :) - Jesus Ramos

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

但对于少于一百万字的字符串,这可能不值得。


通常的教条式评论:没有65,536个字符。Java的本地字符集Unicode有超过一百万个字符的空间,目前已定义了超过109,000个字符。我知道你可能已经知道这个,但每当我看到“65,536个字符”这个短语时,我就会有这种条件反射的反应。UTF-16是有害的 - Ray Toal
Java支持代码点,如果您需要计算这些代码点,则需要一个更大的数组,但方法是相同的。 - Peter Lawrey
是的,保留一个UTF-16码点数组来计数和解决代理项并将其作为后处理步骤转换成真正的超出BMP字符是完全可以接受的。(我应该在之前的抱怨中加入一个笑脸...) :) - Ray Toal

1

使用循环来解决这个问题是最好的选择。在我看来,加速这种操作的最佳方法是将工作负载分成不同的工作单元,并使用不同的处理器处理这些工作单元(例如,如果您有多处理器计算机,则可以使用线程)。


0

通常情况下,你应该以简单明了的方式编写代码,然后进行性能调优,使其尽可能快速。 如果这意味着使用更快的算法,请这样做,但首先要保持简单。 对于像这样的小程序,这并不太难。

性能调优中的关键技能是不要猜测。 相反,让程序自己告诉你需要修复什么。 这是我的方法。

对于更复杂的程序,比如这个, 经验会告诉你如何避免过度思考,因为它往往会导致性能不佳。


0

你需要采用分而治之的方法,避免资源竞争。有不同的方法和/或实现方式。思路是相同的-将工作分割并并行处理。

在单台机器上,你可以在单独的线程中处理数据块,尽管将这些块放在同一磁盘上会显著减慢速度。拥有更多的线程意味着有更多的上下文切换,为了吞吐量更好,我认为最好只使用较少数量的线程并让它们保持繁忙状态。

你可以将处理过程分成阶段,并使用SEDA或类似的东西,对于真正大的数据,你可以使用map-reduce - 只需考虑在集群中分发数据的费用。

如果有人能指出另一个广泛使用的API,我会很高兴。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接