编码非常大的文件时,如何绕过此 EOutOfMemory 异常?

Posted

技术标签:

【中文标题】编码非常大的文件时,如何绕过此 EOutOfMemory 异常?【英文标题】:How Can I Get Around this EOutOfMemory Exception When Encoding a Very Large File? 【发布时间】:2010-06-29 02:14:37 【问题描述】:

我正在使用带有 Unicode 字符串的 Delphi 2009。

我正在尝试对一个非常大的文件进行编码以将其转换为 Unicode:

var
  Buffer: TBytes;
  Value: string;

Value := Encoding.GetString(Buffer);

这适用于 40 MB 的缓冲区,该缓冲区的大小翻倍并以 80 MB Unicode 字符串形式返回值。

当我尝试使用 300 MB 缓冲区时,它给了我一个 EOutOfMemory 异常。

嗯,这并不完全出乎意料。但我还是决定追查它。

它进入系统单元中的 DynArraySetLength 过程。在该过程中,它进入堆并调用 ReallocMem。令我惊讶的是,它成功分配了 665,124,864 字节!!!

但是在接近 DynArraySetLength 的末尾,它调用了 FillChar:

  // Set the new memory to all zero bits
  FillChar((PAnsiChar(p) + elSize * oldLength)^, elSize * (newLength - oldLength), 0);

您可以通过评论看到应该做什么。该例程没有太多内容,但这就是导致 EOutOfMemory 异常的例程。这是来自系统单元的 FillChar:

procedure _FillChar(var Dest; count: Integer; Value: Char);
$IFDEF PUREPASCAL
var
  I: Integer;
  P: PAnsiChar;
begin
  P := PAnsiChar(@Dest);
  for I := count-1 downto 0 do
    P[I] := Value;
end;
$ELSE
asm                                  // Size = 153 Bytes
        CMP   EDX, 32
        MOV   CH, CL                 // Copy Value into both Bytes of CX
        JL    @@Small
        MOV   [EAX  ], CX            // Fill First 8 Bytes
        MOV   [EAX+2], CX
        MOV   [EAX+4], CX
        MOV   [EAX+6], CX
        SUB   EDX, 16
        FLD   QWORD PTR [EAX]
        FST   QWORD PTR [EAX+EDX]    // Fill Last 16 Bytes
        FST   QWORD PTR [EAX+EDX+8]
        MOV   ECX, EAX
        AND   ECX, 7                 // 8-Byte Align Writes
        SUB   ECX, 8
        SUB   EAX, ECX
        ADD   EDX, ECX
        ADD   EAX, EDX
        NEG   EDX
@@Loop:
        FST   QWORD PTR [EAX+EDX]    // Fill 16 Bytes per Loop
        FST   QWORD PTR [EAX+EDX+8]
        ADD   EDX, 16
        JL    @@Loop
        FFREE ST(0)
        FINCSTP
        RET
        NOP
        NOP
        NOP
@@Small:
        TEST  EDX, EDX
        JLE   @@Done
        MOV   [EAX+EDX-1], CL        // Fill Last Byte
        AND   EDX, -2                // No. of Words to Fill
        NEG   EDX
        LEA   EDX, [@@SmallFill + 60 + EDX * 2]
        JMP   EDX
        NOP                          // Align Jump Destinations
        NOP
@@SmallFill:
        MOV   [EAX+28], CX
        MOV   [EAX+26], CX
        MOV   [EAX+24], CX
        MOV   [EAX+22], CX
        MOV   [EAX+20], CX
        MOV   [EAX+18], CX
        MOV   [EAX+16], CX
        MOV   [EAX+14], CX
        MOV   [EAX+12], CX
        MOV   [EAX+10], CX
        MOV   [EAX+ 8], CX
        MOV   [EAX+ 6], CX
        MOV   [EAX+ 4], CX
        MOV   [EAX+ 2], CX
        MOV   [EAX   ], CX
        RET                          // DO NOT REMOVE - This is for Alignment
@@Done:
end;
$ENDIF

所以我的内存被分配了,但是尝试用零填充它时崩溃了。这对我来说没有意义。就我而言,内存甚至不需要用零填充 - 无论如何这可能是浪费时间 - 因为编码语句无论如何都要填充它。

我能以某种方式阻止 Delphi 进行内存填充吗?

或者有没有其他方法可以让 Delphi 为我成功分配这个内存?

我的真正目标是为我的非常大的文件执行编码语句,因此任何允许这样做的解决方案将不胜感激。


结论:请看我的 cmets 的答案。

这是在调试汇编代码时要小心的警告。确保你打破了所有的“RET”行,因为我错过了 FillChar 例程中间的那一行,并错误地认为 FillChar 导致了问题。感谢梅森指出这一点。

我必须将输入分解为块来处理非常大的文件。

【问题讨论】:

【参考方案1】:

FillChar 没有分配任何内存,所以这不是您的问题。尝试跟踪它并在 RET 语句处放置断点,您将看到 FillChar 完成。不管是什么问题,都可能在后面的步骤中。

【讨论】:

谢谢。是的,你是对的。 FillChar 例程中间的 RET 语句是它离开的地方,所以我在例程结束时的休息没有抓住它。然后它会到达 MemoryManager.GetMem 并发出 OutOfMemory 错误信号。我将不得不像@Romain 所说的那样将编码拆分成块。你帮了我,但罗曼回答了我的问题,所以我必须给他接受的答案。【参考方案2】:

从文件中读取一个块,编码并写入另一个文件,重复。

【讨论】:

@Romain:我最初有代码可以做到这一点。但是在将其拆分的边界处很棘手,因为您可能会拆分多字节输入字符。此外,Encoding 例程太快了,不能一次完成所有工作太可惜了。 @Ikessler - 有时您必须在时间或空间上妥协。如果您一次阅读 4k 或更多内容,性能应该不会那么差。 ...甚至一次 40 MB,因为您似乎能够处理。 要做的事情是确保它一次可以处理 100 个字节的块,这使得调试变得容易并且您可以测试边界条件,然后将其设置为非常大的东西(可能是动态的)用于生产代码。 我不会读“块”,我会使用 Stream。带有 readline 的快速 unicode 流应该比 300 mb 的 vm 快得多。【参考方案3】:

一个疯狂的猜测:问题可能是内存被过度使用并且当 FillChar 实际访问内存时它找不到实际给你的页面?我不知道 Windows 是否会过度使用内存,但我知道某些操作系统会——直到你真正尝试使用内存时才会发现它。

如果是这种情况,可能会导致 FillChar 爆炸。

【讨论】:

感谢您的回复,但 FillChar 毕竟不是问题,正如@Mason 指出的那样。【参考方案4】:

程序非常擅长循环。他们不知疲倦地循环,没有抱怨。

分配大量内存需要时间。堆管理器会有很多调用。您的操作系统甚至不知道它是否具有您提前需要的连续内存量。你的操作系统说,是的,我有 1 GB 空闲空间。但是一旦你开始使用它,你的操作系统就会说,等等,你想要所有的东西都放在一块吗?让我确保我有足够的东西在一个地方。如果没有,您会收到错误消息。

如果它确实有内存,那么堆管理器在准备内存并将其标记为已使用方面还有很多工作。

因此,显然,分配更少的内存并简单地循环它是有意义的。这使计算机免于做大量工作,而这些工作只有在完成后才需要撤消。为什么不让它做一点工作来留出你的记忆,然后继续重复使用它呢?

堆栈内存的分配速度比堆内存快得多。如果您保持较小的内存使用量(默认情况下低于 1 MB),编译器可能只使用堆栈内存而不是堆内存,这将使您的循环更快。此外,在寄存器中分配的局部变量非常快。

硬盘集群和缓存大小、CPU 缓存大小等因素可以提供有关最佳块大小的提示。关键是找到一个好的号码。我喜欢使用 64 KB 的块。

【讨论】:

这是一个很好的评论。我将尝试同时使用 40 MB 和 1 MB 作为阻塞大小,并测试更多的堆栈分配是否比更少的堆分配更快。 这个想法是在使用时保持分配的内存,但在堆栈上分配。如果你重复调用一个函数,它在堆栈上分配内存然后释放它,你仍然在做额外的工作。在函数内部使用 for 或 while 循环进行循环以重用内存。

以上是关于编码非常大的文件时,如何绕过此 EOutOfMemory 异常?的主要内容,如果未能解决你的问题,请参考以下文章

核心数据文件损坏时如何绕过警报消息?

在 iOS 终止任务之前,如何让非常大的文件有时间上传到 firebase?

如何在 RNN TensorFlow 中使用非常大的数据集?

如何通过 phpmyadmin 导入非常大的查询?

渗透测试—如何利用文件包含的方式进行攻击

PDFBOX 生成非常大的 PDF/A 文件