在文本文件中求和整数的最快方法

Posted

技术标签:

【中文标题】在文本文件中求和整数的最快方法【英文标题】:Fastest way to sum integers in text file 【发布时间】:2014-09-01 13:21:20 【问题描述】:

问题

假设你有一个大的 ASCII 文本文件,每一行都有一个随机的非负整数,每个整数的范围从 0 到 1,000,000,000。文件中有 100,000,000 行。读取文件并计算所有整数之和的最快方法是什么?

约束:我们有 10MB 的 RAM 可供使用。该文件大小为 1GB,因此我们不想读取整个文件然后对其进行处理。

以下是我尝试过的各种解决方案。我发现结果相当令人惊讶。

我错过了什么更快的东西吗?

请注意:下面给出的所有时间都是为了运行算法总共 10 次(运行一次并丢弃;启动计时器;运行 10 次;停止计时器)。这台机器是相当慢的 Core 2 Duo。

方法一:自然方法

首先要尝试的是显而易见的方法:

private long sumLineByLine() throws NumberFormatException, IOException 
    BufferedReader br = new BufferedReader(new FileReader(file));
    String line;
    long total = 0;
    while ((line = br.readLine()) != null) 
        int k = Integer.parseInt(line);
        total += k;
    
    br.close();
    return total;

请注意,最大可能的返回值是 10^17,它仍然可以轻松放入 long,因此我们不必担心溢出。

在我的机器上,运行 11 次并扣除第一次运行大约需要 92.9 秒

方法 2:小调整

受this question 上的评论启发,我尝试不创建新的int k 来存储解析行的结果,而只是将解析后的值直接添加到total。所以这个:

    while ((line = br.readLine()) != null) 
        int k = Integer.parseInt(line);
        total += k;
    

变成这样:

    while ((line = br.readLine()) != null)
        total += Integer.parseInt(line);

我确信这不会产生任何影响,并认为编译器很可能会为两个版本生成相同的字节码。但是,令我惊讶的是,它确实缩短了一点时间:我们降至 92.1 秒

方法三:手动解析整数

到目前为止,让我困扰的代码是我们将String 转换为int,然后在末尾添加它。在我们进行时添加会不会更快?如果我们自己解析String 会发生什么?像这样的...

private long sumLineByLineManualParse() throws NumberFormatException,
        IOException 
    BufferedReader br = new BufferedReader(new FileReader(file));
    String line;
    long total = 0;
    while ((line = br.readLine()) != null) 
        char chs[] = line.toCharArray();
        int mul = 1;
        for (int i = chs.length - 1; i >= 0; i--) 
            char c = chs[i];
            switch (c) 
            case '0':
                break;
            case '1':
                total += mul;
                break;
            case '2':
                total += (mul << 1);
                break;
            case '4':
                total += (mul << 2);
                break;
            case '8':
                total += (mul << 3);
                break;
            default:
                total += (mul*((byte) c - (byte) ('0')));   
            
            mul*=10;
        
    
    br.close();
    return total;

我认为,这可能会节省一点时间,尤其是在进行乘法运算时进行了一些位移优化。但是转换为字符数组的开销肯定会超过任何收益:现在需要 148.2 秒

方法四:二进制处理

我们可以尝试的最后一件事是将文件作为二进制数据处理。

如果你不知道它的长度,从前面解析一个整数是很尴尬的。向后解析要容易得多:遇到的第一个数字是单位,下一个是十,依此类推。因此,处理整个事情的最简单方法是向后读取文件。

如果我们分配(比如说)8MB 的byte[] 缓冲区,我们可以用文件的最后 8MB 填充它,处理它,然后读取前面的 8MB,依此类推。当我们移动到下一个块时,我们需要小心一点,不要搞砸我们正在解析的数字,但这是唯一的问题。

当我们遇到一个数字时,我们将它(根据它在数字中的位置适当地相乘)加到总数中,然后将系数乘以 10,以便为下一个数字做好准备。如果我们遇到任何不是数字的东西(CR 或 LF),我们只需重置系数。

private long sumBinary() throws IOException 
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[8*1024*1024];
    int mul = 1;
    long total = 0;
    while (lastRead>0) 
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead-len);
        raf.readFully(buf, 0, len);
        lastRead-=len;
        for (int i=len-1; i>=0; i--) 
            //48 is '0' and 57 is '9'
            if ((buf[i]>=48) && (buf[i]<=57)) 
                total+=mul*(buf[i]-48);
                mul*=10;
             else
                mul=1;
        
    
    raf.close();
    return total;

这在 30.8 秒内运行!与之前的最佳速度相比,速度提高了 3 倍

后续问题

    为什么这么快?我期待它会赢,但并没有那么令人印象深刻。主要是转换为String 的开销吗?以及所有关于字符集等的幕后担忧? 我们可以通过使用MappedByteBuffer 提供帮助来做得比这更好吗?我有一种感觉,调用方法从缓冲区读取的开销会减慢速度,尤其是在从缓冲区向后读取时。 向前而不是向后读取文件是否会更好,但仍向后扫描缓冲区?这个想法是您读取文件的第一个块,然后向后扫描,但最后丢弃半数。然后,当您读取下一个块时,您设置偏移量,以便从您丢弃的数字的开头读取。 有什么我没有想到会产生重大影响的事情吗?

更新:更令人惊讶的结果

首先,观察。我以前应该想到过,但我认为基于String 的读取效率低下的原因与其说是创建所有String 对象所花费的时间,不如说是它们的寿命如此短暂:我们有 100,000,000 个垃圾收集器需要处理。那肯定会惹恼它。

现在一些基于人们发布的答案/cmets 的实验。

我是在欺骗缓冲区的大小吗?

一个建议是,由于 BufferedReader 使用 16KB 的默认缓冲区,而我使用了 8MB 的缓冲区,因此我不会将同类与同类进行比较。如果你使用更大的缓冲区,它一定会更快。

这是震惊。 sumBinary() 方法(方法 4)昨天运行了 30.8 秒,缓冲区为 8MB。今天,代码没有改变,风向改变了,我们在 30.4 秒。如果我将缓冲区大小降低到 16KB 以查看它变慢了多少,它变快了!它现在可以在 23.7 秒内运行。疯狂的。谁看到那个来了?!

一些实验表明 16KB 是最佳的。也许 Java 人也做了同样的实验,这就是他们选择 16KB 的原因!

问题是否受 I/O 限制?

我也想知道这个。有多少时间花在磁盘访问上,多少时间花在数字运算上?如果它几乎是所有磁盘访问,正如对建议答案之一的得到充分支持的评论所建议的那样,那么无论我们做什么,我们都无法做出太大的改进。

这很容易通过运行代码来测试,其中所有的解析和数字运算都被注释掉了,但读数仍然完好无损:

private long sumBinary() throws IOException 
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int mul = 1;
    long total = 0;
    while (lastRead > 0) 
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead - len);
        raf.readFully(buf, 0, len);
        lastRead -= len;
        /*for (int i = len - 1; i >= 0; i--) 
            if ((buf[i] >= 48) && (buf[i] <= 57)) 
                total += mul * (buf[i] - 48);
                mul *= 10;
             else
                mul = 1;
        */
    
    raf.close();
    return total;

现在运行 3.7 秒!这在我看来不是 I/O 绑定的。

当然,一些 I/O 速度将来自磁盘缓存命中。但这并不是真正的重点:我们仍然需要 20 秒的 CPU 时间(也使用 Linux 的 time 命令确认),这足够大,可以尝试减少它。

向前而不是向后扫描

我在原始帖子中坚持认为有充分的理由向后而不是向前扫描文件。我没有很好地解释这一点。这个想法是,如果您向前扫描一个数字,您必须累积扫描数字的总值,然后将其添加。如果您向后扫描,您可以随时将其添加到累计总数中。我的潜意识对自己有某种意义(稍后会更多),但我错过了一个关键点,这是在一个答案中指出的:向后扫描,我每次迭代都做两次乘法,但是向前扫描你只需要一个。所以我编写了一个前向扫描版本:

private long sumBinaryForward() throws IOException 
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int fileLength = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int acc = 0;
    long total = 0;
    int read = 0;
    while (read < fileLength) 
        int len = Math.min(buf.length, fileLength - read);
        raf.readFully(buf, 0, len);
        read += len;
        for (int i = 0; i < len; i++) 
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else 
                total += acc;
                acc = 0;
            
        
    
    raf.close();
    return total;

这在 20.0 秒内运行,以一定距离击败后向扫描版本。不错。

乘法缓存

不过,我在晚上意识到,虽然我每次迭代执行两次乘法,但有可能使用缓存来存储这些乘法,这样我就可以避免在向后迭代期间执行它们。当我醒来时看到有人有同样的想法,我很高兴!

关键是我们正在扫描的数字中最多有 10 个数字,并且只有 10 个可能的数字,因此一个数字的值对累积总数只有 100 个可能性。我们可以预先计算这些,然后在向后扫描代码中使用它们。这应该优于前向扫描版本,因为我们现在已经完全摆脱了乘法。 (请注意,我们不能通过前向扫描来执行此操作,因为乘法是累加器的,它可以取高达 10^9 的任何值。只有在向后的情况下,两个操作数都被限制为几种可能性。)

private long sumBinaryCached() throws IOException 
    int mulCache[][] = new int[10][10];
    int coeff = 1;
    for (int i = 0; i < 10; i++) 
        for (int j = 0; j < 10; j++)
            mulCache[i][j] = coeff * j;
        coeff *= 10;
    

    RandomAccessFile raf = new RandomAccessFile(file, "r");
    int lastRead = (int) raf.length();
    byte buf[] = new byte[16 * 1024];
    int mul = 0;
    long total = 0;
    while (lastRead > 0) 
        int len = Math.min(buf.length, lastRead);
        raf.seek(lastRead - len);
        raf.readFully(buf, 0, len);
        lastRead -= len;
        for (int i = len - 1; i >= 0; i--) 
            if ((buf[i] >= 48) && (buf[i] <= 57))
                total += mulCache[mul++][buf[i] - 48];
            else
                mul = 0;
        
    
    raf.close();
    return total;

这会在 26.1 秒内运行。令人失望,至少可以这么说。就 I/O 而言,向后读取效率较低,但我们已经看到 I/O 并不是这里的主要问题。我曾预计这会产生很大的积极影响。也许数组查找与我们替换的乘法一样昂贵。 (我确实尝试将数组设置为 16x16,并使用位移来索引,但没有帮助。)

看起来前向扫描就是它所在的位置。

使用 MappedByteBuffer

接下来要添加的是MappedByteBuffer,看看这是否比使用原始RandomAccessFile 更有效。代码不需要太多改动。

private long sumBinaryForwardMap() throws IOException 
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    byte buf[] = new byte[16 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    int acc = 0;
    long total = 0;
    while (mb.hasRemaining()) 
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        for (int i = 0; i < len; i++)
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else 
                total += acc;
                acc = 0;
            
    
    ch.close();
    raf.close();
    return total;

这似乎确实使情况有所改善:我们现在处于 19.0 秒。我们的个人最好成绩又落后了一秒!

多线程呢?

其中一个建议的答案涉及使用多个内核。我有点惭愧,我没有想到这一点!

因为假设这是一个 I/O 绑定问题,所以答案很简单。鉴于有关 I/O 的结果,这似乎有点苛刻!无论如何,当然值得一试。

我们将使用 fork/join 来执行此操作。这是一个表示文件部分计算结果的类,请记住左侧可能有部分结果(如果我们从数字的一半开始),右侧可能有部分结果(如果缓冲区完成了一个数字的一​​半)。该类还有一个方法允许我们将两个这样的结果粘合在一起,形成两个相邻子任务的组合结果。

private class SumTaskResult 
    long subtotal;
    int leftPartial;
    int leftMulCount;
    int rightPartial;

    public void append(SumTaskResult rightward) 
        subtotal += rightward.subtotal + rightPartial
                * rightward.leftMulCount + rightward.leftPartial;
        rightPartial = rightward.rightPartial;
    

现在是关键位:计算结果的RecursiveTask。对于小问题(小于64个字符),调用computeDirectly()单线程计算结果;对于较大的问题,它会拆分为两个,在单独的线程中解决两个子问题,然后合并结果。

private class SumForkTask extends RecursiveTask<SumTaskResult> 

    private byte buf[];
    // startPos inclusive, endPos exclusive
    private int startPos;
    private int endPos;

    public SumForkTask(byte buf[], int startPos, int endPos) 
        this.buf = buf;
        this.startPos = startPos;
        this.endPos = endPos;
    

    private SumTaskResult computeDirectly() 
        SumTaskResult result = new SumTaskResult();
        int pos = startPos;

        result.leftMulCount = 1;

        while ((buf[pos] >= 48) && (buf[pos] <= 57)) 
            result.leftPartial = result.leftPartial * 10 + buf[pos] - 48;
            result.leftMulCount *= 10;
            pos++;
        

        int acc = 0;
        for (int i = pos; i < endPos; i++)
            if ((buf[i] >= 48) && (buf[i] <= 57))
                acc = acc * 10 + buf[i] - 48;
            else 
                result.subtotal += acc;
                acc = 0;
            

        result.rightPartial = acc;
        return result;
    

    @Override
    protected SumTaskResult compute() 
        if (endPos - startPos < 64)
            return computeDirectly();
        int mid = (endPos + startPos) / 2;
        SumForkTask left = new SumForkTask(buf, startPos, mid);
        left.fork();
        SumForkTask right = new SumForkTask(buf, mid, endPos);
        SumTaskResult rRes = right.compute();
        SumTaskResult lRes = left.join();
        lRes.append(rRes);
        return lRes;
    


请注意,这是在 byte[] 上运行,而不是在整个 MappedByteBuffer 上运行。原因是我们希望保持磁盘访问顺序。我们将获取相当大的块,fork/join,然后移动到下一个块。

这是执行此操作的方法。请注意,我们已将缓冲区大小提高到 1MB(之前不是最理想的,但在这里似乎更合理)。

private long sumBinaryForwardMapForked() throws IOException 
    RandomAccessFile raf = new RandomAccessFile(file, "r");
    ForkJoinPool pool = new ForkJoinPool();

    byte buf[] = new byte[1 * 1024 * 1024];
    final FileChannel ch = raf.getChannel();
    int fileLength = (int) ch.size();
    final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, 0,
            fileLength);
    SumTaskResult result = new SumTaskResult();
    while (mb.hasRemaining()) 
        int len = Math.min(mb.remaining(), buf.length);
        mb.get(buf, 0, len);
        SumForkTask task = new SumForkTask(buf, 0, len);
        result.append(pool.invoke(task));
    
    ch.close();
    raf.close();
    pool.shutdown();
    return result.subtotal;

现在令人沮丧的是:这个漂亮的多线程代码现在需要 32.2 秒。为何这么慢?我花了很长时间调试这个,假设我做错了什么。

事实证明,只需要进行一点小调整。我认为小问题和大问题之间的阈值 64 是合理的;事实证明这完全是荒谬的。

这样想。子问题的大小完全相同,因此它们应该几乎在同一时间完成。因此,实际上没有必要拆分成比可用处理器更多的部分。在我使用的机器上,只有两个内核,降低到 64 的阈值是荒谬的:它只会增加更多的开销。

现在您不想限制事物,即使有更多可用内核,它也只使用两个内核。或许正确的做法是在运行时找出处理器的数量,然后拆分成那么多块。

无论如何,如果我将阈值更改为 512KB(缓冲区大小的一半),它现在会在 13.3 秒内完成。降低到 128KB 或 64KB 将允许使用更多内核(分别最多 8 个或 16 个),并且不会显着影响运行时间。

所以多线程确实有很大的不同。

这是一段相当长的旅程,但我们一开始需要 92.9 秒,现在降至 13.3 秒……这是原始代码的速度的七倍。这不是通过改进渐近(大哦)时间复杂度,它从一开始就是线性(最优)......这一切都是为了改进常数因子。

今天辛苦了。

我想接下来我应该尝试使用 GPU...

后记:生成随机数文件

我使用以下代码生成了随机数,我运行并重定向到了一个文件。显然我不能保证你最终会得到与我完全相同的随机数:)

public static void genRandoms() 
    Random r = new Random();
    for (int i = 0; i < 100000000; i++)
        System.out.println(r.nextInt(1000000000));

【问题讨论】:

它更快,因为您将缓冲区设置为 8mb,而 BufferedReader 使用 16k。即使向后读取文件会更有效率(它不是!),通过将阅读器中的缓冲区增加到相同的级别,您将获得更多。你在比较苹果和橙子。 而且将 FileInputStream 包装到 BufferedInputStream 中,然后在其上创建一个 InputStreamReader (因此缓冲发生在最低级别)可能也会更有效。但是解码字符集仍然是开销。 方法 3:case 在某些情况下使用位移的语句可能会引入比性能改进更多的分支延迟。删除 case 语句并在 for 循环的每次传递中计算 total += (mul*((byte) c - (byte) ('0'))); 。现在的运行时间是多少? 这种程序应该是I/O绑定的,因为读取文件受到机械速度的限制,解析和加法需要一个乘10,一个减法,一个加法。当然,如果你愿意,你可以通过让它分配内存字符串对象来减慢它的速度。 人们总是无法测试他们的假设,即文件处理是 I/O 绑定的。我无法告诉你我听过多少次 C++ fstream 慢并不重要,因为 CPU 比磁盘快得多。基准总是表明这是错误的。要跟上 120MB/s(旋转)或 500MB/s(固态)的现代磁盘流传输,需要非常小心,当然缓存更快。 【参考方案1】:

您的主要瓶颈将是文件 IO。解析和累加数字不应有助于算法,因为可以在文件 I/O 等待磁盘时在单独的线程中完成。

几年前,我研究了如何以最快的方式读取文件,并遇到了一些很好的建议 - 我将其作为扫描例程实现如下:

// 4k buffer size.
static final int SIZE = 4 * 1024;
static byte[] buffer = new byte[SIZE];

// Fastest because a FileInputStream has an associated channel.
private static void ScanDataFile(Hunter p, FileInputStream f) throws FileNotFoundException, IOException 
    // Use a mapped and buffered stream for best speed.
    // See: http://nadeausoftware.com/articles/2008/02/java_tip_how_read_files_quickly
    final FileChannel ch = f.getChannel();
    long red = 0L;
    do 
        final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
        final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
        int nGet;
        while (mb.hasRemaining() && p.ok()) 
            nGet = Math.min(mb.remaining(), SIZE);
            mb.get(buffer, 0, nGet);
            for (int i = 0; i < nGet && p.ok(); i++) 
                p.check(buffer[i]);
                //size += 1;
            
        
        red += read;
     while (red < ch.size() && p.ok());
    // Finish off.
    p.close();
    ch.close();
    f.close();

您可能希望在测试它的速度之前调整此技术,因为它使用称为 Hunter 的接口对象来搜索数据。

如您所见,该建议是在 2008 年得出的,从那时起对 Java 进行了许多增强,因此这可能不会带来任何改进。

添加

我没有对此进行测试,但这应该适合您的测试并使用相同的技术:

class Summer 

    long sum = 0;
    long val = 0;

    public void add(byte b) 
        if (b >= '0' && b <= '9') 
            val = (val * 10) + (b - '0');
         else 
            sum += val;
            val = 0;
        
    

    public long getSum() 
        return sum + val;
    


private long sumMapped() throws IOException 
    Summer sum = new Summer();
    FileInputStream f = new FileInputStream(file);
    final FileChannel ch = f.getChannel();
    long red = 0L;
    do 
        final long read = Math.min(Integer.MAX_VALUE, ch.size() - red);
        final MappedByteBuffer mb = ch.map(FileChannel.MapMode.READ_ONLY, red, read);
        int nGet;
        while (mb.hasRemaining()) 
            nGet = Math.min(mb.remaining(), SIZE);
            mb.get(buffer, 0, nGet);
            for (int i = 0; i < nGet; i++) 
                sum.add(buffer[i]);
            
        
        red += read;
     while (red < ch.size());
    // Finish off.
    ch.close();
    f.close();
    return sum.getSum();

【讨论】:

谢谢!不过,惊讶地发现它根本不受 I/O 限制。我已经相应地更新了帖子...... @chiastic-security 我忍不住用了 1.9 秒实现了similar solution。 @maaartinus 啊,我的时间是运行它十次,虽然:) 我运行一次并丢弃,然后启动计时器,运行十次。在我的机器上使用您的代码执行此操作需要 22.8 秒。 @chiastic-security 我明白了。通过简化条件,我实现了一些速度(1.9 -> 1.4)。我也尝试复制到byte[],但没有帮助。【参考方案2】:

为什么这么快?

创建一个字符串比一点数学要昂贵得多。

我们可以通过使用 MappedByteBuffer 帮助做得比这更好吗?

一点,是的。它是我使用的。它将内存保存到内存副本。即不需要字节[]。

我感觉调用方法从缓冲区读取的开销会减慢速度,

如果方法很简单,它们会被内联。

尤其是从缓冲区向后读取时。

它不会更慢,事实上解析转发更简单/更快,因为您使用一个 * 而不是两个。

向前而不是向后读取文件会更好,但仍然向后扫描缓冲区吗?

我完全不明白你为什么需要向后阅读。

这个想法是您读取文件的第一个块,然后向后扫描,但最后丢弃半数。然后在读取下一个块时,设置偏移量,以便从丢弃的数字的开头读取。

听起来不必要地复杂。我会一次性读取整个文件中的内存映射。除非文件大小为 2+ GB,否则无需使用块。即便如此,我也会一口气读完。

有什么我没有想到会产生重大影响的事情吗?

如果数据在磁盘缓存中,它会比其他任何东西都更重要。

【讨论】:

【参考方案3】:

您可以选择更大的缓冲区大小,以及更快的字符串编码(Unicode)。

BufferedReader br = new BufferedReader(new InputStreamReader(
        new FileInputStream(file), StandardCharsets.US_ASCII),
        1_024_000_000);

您使用二进制 InputStream/RandomAccessFile 消除字符串使用的方法是值得的。

如果源文件被压缩也可能会很好。在 Unix 下,人们会选择 gzip 格式,其中xxx.txt.gz 解压缩为xxx.txt。这可以通过GZipInputStream 阅读。 它的优点是整体加快了与服务器目录之间的文件传输速度。

【讨论】:

【参考方案4】:

我认为还有另一种方法。

这是经典的多进程编程问题。在 C 语言中,有一个库 MPI 可以解决这类问题。

它的想法是将整数列表分成 4 个部分,每个部分由不同的过程求和。完成后,将流程汇总在一起。

在 java 中,这可以通过线程(伪并行)和 java 并发来完成。

例如 4 个不同的线程将列表的 4 个不同部分相加。最后将它们相加。

电话公司使用执行这种并行编程技术的网格计算机来汇总他们的交易。

这里唯一的问题(瓶颈)是 IO 操作。读取文件将花费大量时间。如果您可以通过某种方式让多个线程读取文件的不同部分... 这是非常复杂的方法,我认为这不会有太大的好处,因为磁盘不会因为许多线程使用而旋转得更快,但是还有其他技术可以做类似的事情。您可以在此处阅读更多相关信息:Access File through multiple threads 和此处Reading a single file with Multiple Thread: should speed up?

【讨论】:

多处理几乎无法解决 IO 绑定问题。 在旋转磁盘上,由于额外的寻道,它甚至很有可能变慢。 线程可能有助于加快速度的唯一方法,可能是让一个线程读取数据,另一个处理它(但读取器线程大部分时间都在休眠等待操作系统从磁盘获取数据) 事实证明它不受 I/O 限制,多线程有很大帮​​助!查看更新以发布。 @J.F.Sebastian 这太令人惊讶了。我想,我会尝试提供一个解决方案,花费与阅读文件一样多的时间。还是您认为问题已经解决了?【参考方案5】:

来源:http://nadeausoftware.com/articles/2008/02/java_tip_how_read_files_quickly

为了获得最佳的 Java 读取性能,需要记住以下四点:

通过一次读取一个数组而不是一次读取一个字节来最小化 I/O 操作。一个 8Kbyte 的数组是一个不错的大小。 通过一次获取一个数组而不是一次获取一个字节来最小化方法调用。使用数组索引来获取数组中的字节。 如果您不需要线程安全,请尽量减少线程同步锁。要么减少对线程安全类的方法调用,要么使用 像 FileChannel 和 MappedByteBuffer 这样的非线程安全类。 尽量减少 JVM/OS、内部缓冲区和应用程序阵列之间的数据复制。将 FileChannel 与内存映射一起使用,或者直接 或包装数组 ByteBuffer。

【讨论】:

【参考方案6】:

基于this comment:“简单地总结所有字节更快”,我提出了接受答案的变体。

接受的答案建议将问题分解成块,使用多线程计算每个卡盘的总和,然后在最后将它们加在一起。

这个想法可用于将反向扫描中的乘法次数减少到 O(1),无需任何表查找和线程(或将其与线程结合)。只需利用乘法在加法上的分布方式,将所有个位加到一个累加器中,将十位加到一个单独的累加器中,将成百上千加到它们自己的累加器中。这不需要任何乘法。

reduce 步骤组合来自多个线程的结果也可以使用 per-place 累加器来完成。计算总数的最后一步将需要乘法(或利用 10 仅设置两个位的事实并使用位移和加法),但只有 9 次乘法就足够了。

【讨论】:

是的,这是我的第一个想法。 123 + 456 + 78 = (1+4)*100 + (2+5+7)*10 + (3+6+8)*1。您将有 10 个长整数,每个代表具有相同大小的所有数字的总和,然后最后您将它们总结为:total = l0 + l1*10 + ... + l9 * 1000000000 另外,保留 10 个单独的 long 变量,而不是 10 个 long 的数组,可能有助于提高性能,因为所有数组访问都在 Java 中检查。 (这是我之前在进行此类微优化时遇到的问题。)【参考方案7】:

这里有几个问题。

    任何基于阅读行的解决方案都会对每个字符进行两次处理。例如,编译器不这样做,他们一次读取一个字符并直接发送它。 任何基于readLine() 的解决方案都将创建字符串。 您正在使用不同的缓冲区大小。 您正在使用不同的 I/O 技术。 在某些情况下您会使用字符转换,而在其他情况下则不是。 您过度分析了文件。你并不关心空白在哪里,或者有多少,只要它将数字彼此分开即可。

我的解决方案:

    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 8*1024*1024/2);
    long    total = 0;
    int i;
    while ((i = bis.read()) != -1)
    
        byte    b = (byte)i;
        long    number = 0;
        while (b >= '0' && b <= '9')
        
            number = number*10+b-'0';
            if ((i = bis.read()) == -1)
                break;
            b = (byte)i;
        
        total += number;
    

【讨论】:

以上是关于在文本文件中求和整数的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

delphi写多行文本文件操作

打开 VS Code 并打开一个空白文本文件

打开多行文本文件并仅读取其中的特定行。并将值发送到文本框

使用 192/256 位整数对无符号 64 位整数向量的点积求和的最快方法?

如何在 Python 中对大文本文件流进行过滤和排序

读取一行文本文件,拆分为数组