我的代码为什么会占用这么多内存?

4

我已经通过不同的方式解决了CodeEval上一个简单的问题,其规范可以在这里找到(只有几行)。

我已经制作了3个工作版本(其中一个是Scala),但我不明白为什么我的最新Java版本在时间和内存方面表现不如预期。

我还将其与在Github上找到的代码进行了比较。以下是CodeEval返回的性能统计信息:

Stats

版本1是在Github上找到的版本。版本2是我的Scala解决方案:

object Main extends App {
    val p = Pattern.compile("\\d+")
    scala.io.Source.fromFile(args(0)).getLines
        .filter(!_.isEmpty)
        .map(line => {
            val dists = new TreeSet[Int]
            val m     = p.matcher(line)
            while (m.find) dists += m.group.toInt

            val list  = dists.toList
            list.zip(0 +: list).map { case (x,y) => x - y }.mkString(",")
        })
        .foreach(println)
}

版本3是我的Java解决方案,我认为它是最好的:

public class Main {
    public static void main(String[] args) throws IOException {
        Pattern        p    = Pattern.compile("\\d+");
        File           file = new File(args[0]);
        BufferedReader br   = new BufferedReader(new FileReader(file));

        String line;
        while ((line = br.readLine()) != null) {
            Set<Integer> dists = new TreeSet<Integer>();
            Matcher      m     = p.matcher(line); 
            while (m.find()) dists.add(Integer.parseInt(m.group()));

            Iterator<Integer> it   = dists.iterator();
            int               prev = 0;
            StringBuilder     sb   = new StringBuilder();
            while (it.hasNext()) {
                int curr = it.next();
                sb.append(curr - prev);
                sb.append(it.hasNext() ? "," : "");
                prev = curr;
            }
            System.out.println(sb);
        }
        br.close();
    }
}
  • 版本4与版本3相同,只是我不使用 StringBuilder 来打印输出,并像版本1一样执行

这是我对这些结果的解释:

  • 版本1由于System.out.print调用次数过多而速度太慢。此外,在非常大的行上使用split(在执行的测试中就是这种情况)会占用大量内存。

  • 版本2似乎也很慢,但主要是因为在CodeEval上运行Scala代码的“开销”,即使非常高效的代码也会运行缓慢。

  • 版本2使用不必要的内存来从集合构建列表,这也需要一些时间,但应该不会太重要。编写更高效的Scala可能像编写Java一样,所以我更喜欢优雅而不是性能。

  • 版本3在我看来不应该使用那么多内存。使用StringBuilder对内存的影响与在版本2中调用mkString相同。

  • 版本4证明了对System.out.println的调用正在减慢程序的速度。

有人能解释这些结果吗?


1
我开始觉得CodeEval的统计数据可能不太可靠... - Dici
请注意,在版本3中,每次都会创建一个新的StringBuilder。默认构造函数仅获得长度为16的内部缓冲区。如果您要构建长字符串,则最好重复使用单个StirngBuilder而不是每次重新创建它。这可能会在JVM发出GC之前占用大量内存。您可以使用StringBuilder.setLength(0)来清除缓冲区并重用它。您还为每行重新创建了Set <Integer>。 - Alex Suo
@Dici 你确定Scala使用了相同的Pattern.compile底层吗?因为如果你看一下那段代码,它会在内存中生成大量垃圾。 - Alex Suo
@OldCurmudgeon 是的,我按照AlexSuo的消息移动了它。这些行确实非常长,但逐行阅读是我在任何版本中能做到的最少要求。我会尝试使用不同类型的阅读器。 - Dici
我开始使用我选择的编程语言(R)在CodeEval上进行游戏,同时试图回答这个问题:http://codereview.stackexchange.com/questions/86939/reducing-memory-usage-for-fizzbuzz-in-r。我的印象是报告的内存使用量包括运行进程所需的内存。只是启动一个R进程就需要约30mb的内存,所以我编写的任何R脚本的内存使用量都至少是这个数量。也许在你们的情况下,Java进程需要比Scala更多的内存?我希望你们的测试能够证实我的假设。如果CodeEval是这样工作的,那就太遗憾了... - flodel
显示剩余8条评论
3个回答

3

我进行了一些测试。

��语言都有一个基准。我编写Java和JavaScript代�。以下是我的JavaScript测试结�:

random challenge

  • Rev 1:JS的默认空白模æ�¿ï¼Œå¹¶åœ¨æ ‡å‡†è¾“出上显示消æ�¯
  • Rev 2:å�Œæ ·æ²¡æœ‰æ–‡ä»¶è¯»å�–
  • Rev 3:仅å�‘标准输出å�‘é€�消æ�¯

无论如何,您�以看到至少需�200毫秒�行时间和约5兆字节的内存使用�。 这个基准还�决��务器负载�曾�有一段时间,codeevals过载,因此无法在最大时间(10秒)内�行任何内容。

看看这个,�先�完全��的挑战:

Random pic 2.
  • Rev4:我的解决方案
  • Rev5:相å�Œçš„代ç �å†�次æ��交。 è�·å¾—了8000个æ�’å��点。 😀

结论:��太担心CPU和内存使用�以���。 显然���。


哈哈,我明白了... 我也注意到许多允许你运行自己编写代码的网站要比其他语言花费更长的时间来运行 Scala 代码(最快的是像 JS 或 Python 这样的脚本语言,这是合理的)。这令我感到惊讶,因为虽然 Scala 编译器很慢,但对于像这样非常小的代码而言,应该不会太昂贵,没有什么花哨的东西。无论如何,当时我的结论与你的相同:它不可靠。我会把这个当作答案。 - Dici

2
你的Scala解决方案速度较慢,并不是因为“在CodeEval上的开销”,而是因为你正在构建一个不可变的TreeSet,并逐个添加元素。建议替换为类似以下的内容:
val regex = """\d+""".r // in the beginning, instead of your Pattern.compile
...
.map { line => 
    val dists = regex.findAllIn(line).map(_.toInt).toIndexedSeq.sorted
...

可以将执行时间缩短约30-40%。

相同的方法(构建列表,然后排序)可能会在“版本3”中帮助您的内存利用率(Java集合是真正的内存占用者)。在此过程中为列表设置初始大小也是一个好主意(否则,每次运行时都会增加50%,这对内存和性能都是浪费的)。 600听起来像是一个不错的数字,因为它是问题描述中城市数量的上限。

现在,由于我们知道了上限,更快,更轻的方法是放弃列表和装箱整数,只需执行int dists [] = new int [600];

如果你想变得更加复杂,你还可以利用问题描述中提到的“路径长度”范围。例如,不要将int放入数组并进行排序(或保留treeset),而是制作一个20,000位(甚至20K字节以提高速度)的数组,并在读取输入时设置这些位...这比您的任何解决方案都更快,更节省内存。


您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Dici
可变集合确实更好,但它仍然不如先累积数据,然后再排序。两者的时间复杂度都是O(nlog(n)),但常数差异显着。当需要在数据到来时维护有序数据时,TreeSet可能是一个不错的选择。在这种情况下,只需读取一次,然后进行排序肯定会更快。关于版本1与版本3的区别,并不在于正则表达式。版本1没有使用StringBuilder,这个字符串会变得非常庞大。我认为,如果你在V3中去掉TreeSet,即使保持StringBuilder不变,你也可以轻松地击败V1的占用空间。 - Dima
我使用了 LinkedList 而不是集合。它在时间和内存上的表现相当,稍微慢一些,占用的内存略多,但这可能是由于输入数据大小略有不同。我真的认为 CodeEval 的内存测试有问题,因为 LinkedListTreeSet 更小的内存开销,不是吗? - Dici
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Dici
好的,感谢您的时间。我为您提供的好建议点赞,但我不接受您的答案,因为它没有解释代码之间的区别。尽管如此,我认为由于统计数据似乎存在偏差,所以可能无法回答。再次感谢! - Dici
显示剩余2条评论

1

我尝试解决这个问题并发现您不需要城市的名称,只需要按排序后的距离数组。

使用此方法可以获得更好的运行时间(738毫秒)和内存(4513792)。

虽然这可能无法帮助改进您的代码,但似乎是更好的解决问题的方法。欢迎提出进一步改进代码的建议。

import java.io.*;
import java.util.*;

public class Main {
public static void main (String[] args) throws IOException {
    File file = new File(args[0]);
    BufferedReader buffer = new BufferedReader(new FileReader(file));
    String line;
    while ((line = buffer.readLine()) != null) {
        line = line.trim();

        String out = new Main().getDistances(line); 

        System.out.println(out);
    }
}

public String getDistances(String s){

    //split the string
    String[] arr = s.split(";");

    //create an array to hold the distances as integers
    int[] distances = new int[arr.length];

    for(int i=0; i<arr.length; i++){
         //find the index of , - get the characters after that - convert to integer - add to distances array
        distances[i] = Integer.parseInt(arr[i].substring(arr[i].lastIndexOf(",")+1));    
    }

    //sort the array
    Arrays.sort(distances);

    String output = "";
    output += distances[0]; //append the distance to the closest city to the string

    for(int i=0; i<arr.length-1; i++){

        //get distance between current element(city) and next
        int distance_between = distances[i+1] - distances[i];

        //append the distance to the string
        output += "," + distance_between;
    }


    return output;   
 }
 }

我还没有深入查看你的代码,但如果你阅读我的代码,你会发现我从来没有存储城市的名称。 - Dici
1
你为每次迭代创建一个新的 Main,而你的类是无状态的:你可以一直使用相同的实例,甚至更好的做法是在这种情况下使用一个 static 方法。在这里不需要一个类,也没有概念需要建模。 - Dici
1
最后,您可以使用 += 连接字符串,但对于大型字符串来说可能非常昂贵。请改用 StringBuilder - Dici

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