使用Java高效获取文件大小

171

8
你能提供一些说明 File.length() "可能会很慢" 的链接吗? - matt b
1
抱歉,这里是链接 http://www.javaperformancetuning.com/tips/rawtips.shtml搜索以下内容: “文件信息(例如File.length())需要系统调用,可能会很慢。”这是一个令人困惑的陈述,似乎几乎可以假定它是一个系统调用。 - joshjdevl
27
无论如何获取文件长度都需要进行系统调用,即使采用其他方式也不例外。如果在网络或某些非常慢的文件系统上,可能会很慢。使用File.length()没有更快的方法来获得它,这里定义的“慢”只是指不要不必要地调用它。 - jsight
1
如果信息存储在磁盘上而不是缓存中,这个操作可能会非常慢(例如慢1000倍)。然而,除了确保所需信息始终在缓存中(例如预加载它并具有足够的内存使其保留在内存中)之外,你几乎无法做什么来解决这个问题。 - Peter Lawrey
我会质疑在这个问题被提出时已经有8/9年历史的文档作为优化建议的来源的有效性。 - Burhan Ali
显示剩余3条评论
9个回答

103

好的,我尝试使用以下代码对其进行测量:

当runs = 1且iterations = 1时,URL方法大多数情况下是最快的,其次是channel。我做了大约10次暂停的新测试。因此,对于一次访问,使用URL是我能想到的最快的方法:

LENGTH sum: 10626, per Iteration: 10626.0

CHANNEL sum: 5535, per Iteration: 5535.0

URL sum: 660, per Iteration: 660.0

当runs = 5和iterations = 50时,图片呈现出不同的绘制效果。

LENGTH sum: 39496, per Iteration: 157.984

CHANNEL sum: 74261, per Iteration: 297.044

URL sum: 95534, per Iteration: 382.136

文件必须缓存对文件系统的调用,而通道和URL具有某些开销。

代码:

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

public enum FileSizeBench {

    LENGTH {
        @Override
        public long getResult() throws Exception {
            File me = new File(FileSizeBench.class.getResource(
                    "FileSizeBench.class").getFile());
            return me.length();
        }
    },
    CHANNEL {
        @Override
        public long getResult() throws Exception {
            FileInputStream fis = null;
            try {
                File me = new File(FileSizeBench.class.getResource(
                        "FileSizeBench.class").getFile());
                fis = new FileInputStream(me);
                return fis.getChannel().size();
            } finally {
                fis.close();
            }
        }
    },
    URL {
        @Override
        public long getResult() throws Exception {
            InputStream stream = null;
            try {
                URL url = FileSizeBench.class
                        .getResource("FileSizeBench.class");
                stream = url.openStream();
                return stream.available();
            } finally {
                stream.close();
            }
        }
    };

    public abstract long getResult() throws Exception;

    public static void main(String[] args) throws Exception {
        int runs = 5;
        int iterations = 50;

        EnumMap<FileSizeBench, Long> durations = new EnumMap<FileSizeBench, Long>(FileSizeBench.class);

        for (int i = 0; i < runs; i++) {
            for (FileSizeBench test : values()) {
                if (!durations.containsKey(test)) {
                    durations.put(test, 0l);
                }
                long duration = testNow(test, iterations);
                durations.put(test, durations.get(test) + duration);
                // System.out.println(test + " took: " + duration + ", per iteration: " + ((double)duration / (double)iterations));
            }
        }

        for (Map.Entry<FileSizeBench, Long> entry : durations.entrySet()) {
            System.out.println();
            System.out.println(entry.getKey() + " sum: " + entry.getValue() + ", per Iteration: " + ((double)entry.getValue() / (double)(runs * iterations)));
        }

    }

    private static long testNow(FileSizeBench test, int iterations)
            throws Exception {
        long result = -1;
        long before = System.nanoTime();
        for (int i = 0; i < iterations; i++) {
            if (result == -1) {
                result = test.getResult();
                //System.out.println(result);
            } else if ((result = test.getResult()) != result) {
                 throw new Exception("variance detected!");
             }
        }
        return (System.nanoTime() - before) / 1000;
    }

}

76
stream.available() 方法不会返回文件长度,而是返回当前可供读取的字节数,而不会阻塞其他流的读取。它返回的字节数未必与文件长度相同。如果想要得到流的实际长度,你需要读取(并在读取时统计字节数)。 - BalusC
1
@GHad,那你做错了。API中没有规定这种行为。你正在依赖运气。 - user207421
12
这个基准测试或者说它的解释不正确。在低迭代次数中,后面的测试利用了操作系统的文件缓存。在更高的迭代次数中,排名是正确的,但并不是因为 File.length() 缓存了某些东西,而只是因为其他两个选项使用相同的方法,但做了额外的工作,使它们变慢。 - x4u
2
@Paolo,缓存和优化文件系统访问是操作系统的主要职责之一。为了获得良好的基准测试结果,在每次运行前应清除缓存。http://www.faqs.org/docs/linux_admin/buffer-cache.html - z0r
3
除了InputStream.available()的Java文档所述之外,该方法返回int应该是针对URL方法的一个警告信号。尝试使用3GB文件进行操作,很明显这不是一种确定文件长度的有效方法。 - Scrubbie
显示剩余8条评论

32

GHad给出的基准测试测量了许多其他内容(例如反射、实例化对象等),而不仅仅是获取长度。如果我们试图摆脱这些东西,那么对于一个调用,我得到以下微秒时间:

   文件总和___19.0,每次迭代___19.0
    raf总和___16.0,每次迭代___16.0
通道总和__273.0,每次迭代__273.0

对于100次运行和10000次迭代,我得到:

   文件总和__1767629.0,每次迭代__1.7676290000000001
    raf总和___881284.0,每次迭代__0.8812840000000001
通道总和___414286.0,每次迭代__0.414286

我运行了以下修改后的代码,将一个100MB文件的名称作为参数。

import java.io.*;
import java.nio.channels.*;
import java.net.*;
import java.util.*;

public class FileSizeBench {

  private static File file;
  private static FileChannel channel;
  private static RandomAccessFile raf;

  public static void main(String[] args) throws Exception {
    int runs = 1;
    int iterations = 1;

    file = new File(args[0]);
    channel = new FileInputStream(args[0]).getChannel();
    raf = new RandomAccessFile(args[0], "r");

    HashMap<String, Double> times = new HashMap<String, Double>();
    times.put("file", 0.0);
    times.put("channel", 0.0);
    times.put("raf", 0.0);

    long start;
    for (int i = 0; i < runs; ++i) {
      long l = file.length();

      start = System.nanoTime();
      for (int j = 0; j < iterations; ++j)
        if (l != file.length()) throw new Exception();
      times.put("file", times.get("file") + System.nanoTime() - start);

      start = System.nanoTime();
      for (int j = 0; j < iterations; ++j)
        if (l != channel.size()) throw new Exception();
      times.put("channel", times.get("channel") + System.nanoTime() - start);

      start = System.nanoTime();
      for (int j = 0; j < iterations; ++j)
        if (l != raf.length()) throw new Exception();
      times.put("raf", times.get("raf") + System.nanoTime() - start);
    }
    for (Map.Entry<String, Double> entry : times.entrySet()) {
        System.out.println(
            entry.getKey() + " sum: " + 1e-3 * entry.getValue() +
            ", per Iteration: " + (1e-3 * entry.getValue() / runs / iterations));
    }
  }
}

3
实际上,虽然你说它衡量了其他方面是正确的,但我应该在我的问题上更加清晰明确。我想获取多个文件的文件大小,并且希望使用最快捷的方式。因此,我确实需要考虑对象创建和开销,因为这是一个实际情况。 - joshjdevl
3
大约有90%的时间花费在那个“getResource”上。我怀疑你不需要使用反射来获取包含一些Java字节码的文件的名称。 - anon

21

这篇文章中的所有测试用例都存在缺陷,因为它们针对每个被测试的方法访问同一个文件。因此磁盘缓存会发挥作用,从而使得测试2和3受益。为了证明我的观点,我采用了GHAD提供的测试用例,并更改了枚举的顺序,以下是结果。

从结果来看,我认为File.length()才是真正的赢家。

测试的顺序就是输出的顺序。甚至可以看到在我的机器上执行时所花费的时间会因不同的执行而有所变化,但当File.Length()不是第一个并且首次访问磁盘时,它获胜了。

---
LENGTH sum: 1163351, per Iteration: 4653.404
CHANNEL sum: 1094598, per Iteration: 4378.392
URL sum: 739691, per Iteration: 2958.764

---
CHANNEL sum: 845804, per Iteration: 3383.216
URL sum: 531334, per Iteration: 2125.336
LENGTH sum: 318413, per Iteration: 1273.652

--- 
URL sum: 137368, per Iteration: 549.472
LENGTH sum: 18677, per Iteration: 74.708
CHANNEL sum: 142125, per Iteration: 568.5

9
当我修改您的代码,使用绝对路径访问文件而不是资源时,我会得到不同的结果(对于1个运行、1个迭代和100,000字节的文件——10个字节文件的时间相同)
长度总和:33,每次迭代:33.0
通道总和:3626,每次迭代:3626.0
URL总和:294,每次迭代:294.0

9

回应rgrig的基准测试,需要考虑打开/关闭FileChannel和RandomAccessFile实例所需的时间,因为这些类将打开一个流以读取文件。

修改基准测试后,我得到了对于85MB文件的1次迭代的以下结果:

file totalTime: 48000 (48 us)
raf totalTime: 261000 (261 us)
channel totalTime: 7020000 (7 ms)

针对同一文件进行10000次迭代:

file totalTime: 80074000 (80 ms)
raf totalTime: 295417000 (295 ms)
channel totalTime: 368239000 (368 ms)

如果你只需要文件大小,使用file.length()是最快的方法。如果你打算将文件用于其他目的,如读写操作,则RAF似乎更好。只是不要忘记关闭文件连接:-)

import java.io.File;
import java.io.FileInputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Map;

public class FileSizeBench
{    
    public static void main(String[] args) throws Exception
    {
        int iterations = 1;
        String fileEntry = args[0];

        Map<String, Long> times = new HashMap<String, Long>();
        times.put("file", 0L);
        times.put("channel", 0L);
        times.put("raf", 0L);

        long fileSize;
        long start;
        long end;
        File f1;
        FileChannel channel;
        RandomAccessFile raf;

        for (int i = 0; i < iterations; i++)
        {
            // file.length()
            start = System.nanoTime();
            f1 = new File(fileEntry);
            fileSize = f1.length();
            end = System.nanoTime();
            times.put("file", times.get("file") + end - start);

            // channel.size()
            start = System.nanoTime();
            channel = new FileInputStream(fileEntry).getChannel();
            fileSize = channel.size();
            channel.close();
            end = System.nanoTime();
            times.put("channel", times.get("channel") + end - start);

            // raf.length()
            start = System.nanoTime();
            raf = new RandomAccessFile(fileEntry, "r");
            fileSize = raf.length();
            raf.close();
            end = System.nanoTime();
            times.put("raf", times.get("raf") + end - start);
        }

        for (Map.Entry<String, Long> entry : times.entrySet()) {
            System.out.println(entry.getKey() + " totalTime: " + entry.getValue() + " (" + getTime(entry.getValue()) + ")");
        }
    }

    public static String getTime(Long timeTaken)
    {
        if (timeTaken < 1000) {
            return timeTaken + " ns";
        } else if (timeTaken < (1000*1000)) {
            return timeTaken/1000 + " us"; 
        } else {
            return timeTaken/(1000*1000) + " ms";
        } 
    }
}

8
我遇到了同样的问题。我需要获取网络共享驱动器上90,000个文件的文件大小和修改日期。使用Java,尽可能地简化代码,这将花费很长时间。(我需要从文件中获取URL,并获取对象的路径。因此,它有些不同,但需要超过一个小时。)然后我使用本机Win32可执行文件完成相同的任务,只是将文件路径、修改日期和大小转储到控制台,并从Java中执行。速度非常快。本机进程和我的字符串处理程序可以每秒处理1000多个项目。
因此,即使人们对上面的评论进行了贬低,这仍然是一个有效的解决方案,并且解决了我的问题。在我的情况下,我提前知道我需要大小的文件夹,并可以将其传递给我的win32应用程序的命令行。我从处理目录需要几个小时到几分钟。
问题似乎也是Windows特有的。OS X没有同样的问题,可以像操作系统一样快速访问网络文件信息。
在Windows上,Java文件处理非常糟糕。对于本地磁盘文件的访问很好。只有网络共享导致了糟糕的性能。Windows也可以在不到一分钟的时间内获取网络共享的信息并计算总大小。
--Ben

3
如果您想要获取目录中多个文件的文件大小,请使用Files.walkFileTree。您可以从收到的BasicFileAttributes中获得大小。
这比在File.listFiles()的结果上调用.length()或在Files.newDirectoryStream()的结果上使用Files.size()要快得多。在我的测试案例中,它快了大约100倍。

请注意,Files.walkFileTree 仅适用于 Android 26+。 - Joshua Pinter

2
实际上,我认为"ls"可能更快。在Java中处理获取文件信息时肯定存在一些问题。不幸的是,对于Windows来说,没有等效的安全递归ls方法。(cmd.exe的DIR /S可能会混淆并在无限循环中产生错误)
在XP上,访问LAN上的服务器,在Windows中获取文件夹中文件数量(33,000)和总大小需要5秒钟。
当我在Java中递归迭代时,这需要超过5分钟的时间。我开始测量执行file.length()、file.lastModified()和file.toURI()所需的时间,我发现我的99%时间都花费在这三个调用上。实际上我只需要进行这三个调用...
1000个文件的差异本地为15毫秒,服务器上为1800毫秒。在Java中扫描服务器路径速度非常慢。如果本机操作系统能够快速扫描同一文件夹,为什么Java就不能呢?
作为更完整的测试,我使用Windows上的WineMerge比较了修改日期和服务器上的文件大小与本地文件的大小。这涉及到每个文件夹中33000个文件的整个目录树的迭代。总时间7秒。Java:超过5分钟。
因此,原始语句和OP的问题是正确且有效的。在处理本地文件系统时,这不那么明显。使用WinMerge进行33,000项目文件夹的本地比较需要3秒,在Java中需要32秒。因此,在这些基本测试中,Java与本地相比慢10倍。
Java 1.6.0_22(最新版本),千兆位局域网和网络连接,ping时间小于1毫秒(均在同一交换机中)
Java很慢。

2
这似乎也是特定于操作系统的。在使用Samba从OS X访问相同文件夹的情况下,执行相同的Java应用程序需要26秒钟来列出全部33,000个项目、大小和日期。那么网络上的Java在Windows上只是慢吗?(OS X也是Java 1.6.0_22。) - Ben Spink

2

根据GHad的基准测试,有一些问题被提到:

1>像BalusC所提到的那样:在这种情况下,stream.available()存在问题。

因为available()返回一个估计值,即在不阻塞此输入流的下一次调用方法的情况下可以从该输入流中读取(或跳过)的字节数。

因此,首先要排除此方法以删除URL。

2>正如StuartH所提到的 - 测试运行的顺序也会造成缓存差异,因此需要单独运行测试来消除这一影响。


现在开始测试:

当CHANNEL 1单独运行时:

CHANNEL sum: 59691, per Iteration: 238.764

当LENGTH只有一个运行时:

LENGTH sum: 48268, per Iteration: 193.072

看起来LENGTH是这里的赢家:

@Override
public long getResult() throws Exception {
    File me = new File(FileSizeBench.class.getResource(
            "FileSizeBench.class").getFile());
    return me.length();
}

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