PowerShell - System.OutOfMemoryException

PowerShell - System.OutOfMemoryException

我想要Get-Content将一个大文件(1GB - 10GB).txt(只有 1 行!)拆分为多个包含多行的文件,但每当我尝试这样做时,最终都会得到一个System.OutOfMemoryException.

当然,我确实寻找了解决方案,但我发现的所有解决方案都是逐行读取文件,当文件只有一行时,这有点困难。

尽管 PowerShell 在加载 1 GB 文件时最多占用 4 GB 的 RAM,但该问题与我的 RAM 无关,因为我总共有 16 GB,即使在后台运行游戏,峰值使用率也在 60% 左右。

我正在使用带有 PowerShell 5.1(64 位)的 Windows 10,并且我的MaxMemoryPerShellMB设置为默认值2147483647


这是我编写并正在使用的脚本,它在文件大小为 100MB 的情况下运行良好:

$source = "C:\Users\Env:USERNAME\Desktop\Test\"
$input = "test_1GB.txt"
$temp_dir = "_temp"

# 104'857'600 bytes (or characters) are exactly 100 MB, so a 1 GB file has exactly
# 10 temporary files, which have all the same size, and amount of lines and line lenghts.

$out_size = 104857600

# A line length of somewhere around 18'000 characters seems to be the sweet spot, however
# the line length needs to be dividable by 4 and at best fit exactly n times into the
# temporary file, so I use 16'384 bytes (or characters) which is exactly 16 KB.

$line_length = 16384



$file = (gc $input)
$in_size = (gc $input | measure -character | select -expand characters)
if (!(test-path $source$temp_dir)) {ni -type directory -path "$source$temp_dir" >$null 2>&1}

$n = 1
$i = 0

if ($out_size -eq $in_size) {
    $file -replace ".{$line_length}", "$&`r`n" | out-file -filepath "$temp_dir\_temp_0001.txt" -encoding ascii
} else {
    while ($i -le ($in_size - $out_size)) {
        $new_file = $file.substring($i,$out_size)
        if ($n -le 9) {$count = "000$n"} elseif ($n -le 99) {$count = "00$n"} elseif ($n -le 999) {$count = "0$n"} else {$count = $n}
        $temp_name = "_temp_$count.txt"
        $i += $out_size
        $n += 1
        $new_file -replace ".{$line_length}", "$&`r`n" | out-file -filepath "$temp_dir\$temp_name" -encoding ascii
    }
    if ($i -ne $in_size) {
        $new_file = $file.substring($i,($in_size-$i))
        if ($n -le 9) {$count = "000$n"} elseif ($n -le 99) {$count = "00$n"} elseif ($n -le 999) {$count = "0$n"} else {$count = $n}
        $temp_name = "_temp_$count.txt"
        $new_file -replace ".{$line_length}", "$&`r`n" | out-file -filepath "$temp_dir\$temp_name" -encoding ascii
    }
}

如果有更简单且不需要使用的解决方案,Get-Content我也很乐意接受。只要可以在每台最新的 Windows 机器上实现,并且不需要额外的软件,我如何实现结果并不重要。但是,如果这不可能,我也会考虑其他解决方案。

答案1

将大文件读入内存只是为了分割它们,虽然很容易,但永远不是最有效的方法,而且你会遇到内存限制某处

这里这一点更加明显,因为Get-Content它适用于字符串 — — 而且,正如您在评论中提到的,您正在处理二进制文件。

.NET(以及 PowerShell)将所有字符串以 UTF-16 代码单元的形式存储在内存中。这意味着每个代码单元在内存中占用 2 个字节。

单个 .NET 字符串只能存储 (2^31 - 1) 个代码单元,因为字符串的长度由Int32(即使在 64 位版本中也是如此) 跟踪。将其乘以 2,单个 .NET 字符串 (理论上) 可以使用大约 4 GB。

Get-Content将把每一行存储在自己的字符串中。如果一行有超过 20 亿个字符...这很可能就是为什么尽管有“足够”的内存,您仍会收到该错误的原因。

或者,可能是因为任何给定对象的大小限制为 2 GB除非明确启用了更大的容量(它们是用于 PowerShell 的吗?)。你的 4 GB OOM可以也可能是因为有两个副本/缓冲区保留在周围以Get-Content尝试找到要分割的换行符。

当然,解决方案是使用字节而不是字符(字符串)。


如果您想避免使用第三方程序,最好的方法是使用 .NET 方法。使用 C# 等完整语言(可以嵌入到 PowerShell 中)最容易做到这一点,但也可以完全使用 PS 来实现。

这个想法是你想使用字节数组,而不是文本流。有两种方法可以做到这一点:

  • 使用[System.IO.File]::ReadAllBytes[System.IO.File]::WriteAllBytes。这非常简单,而且比字符串更好(无需转换,不会占用 2 倍内存),但在处理非常大的文件时仍然会遇到问题 - 比如说你想处理 100 GB 的文件?

  • 使用文件流并以较小的块进行读取/写入。这需要更多的数学运算,因为您需要跟踪您的位置,但可以避免一次性将整个文件读入内存。这可能是最快的方法:分配非常大的对象可能会超过多次读取的开销。

因此,您可以读取合理大小的块(目前,最小块为 4kB),并将它们一次一个块地复制到输出文件中,而不是将整个文件读入内存并进行拆分。如果您需要榨干每一滴性能,您可能希望将大小调高,例如 8kB、16kB、32kB 等 - 但您需要进行基准测试以找到最佳大小,因为某些较大的大小会更慢。

下面是一个示例脚本。为了便于重复使用,应该将其转换为 cmdlet 或至少 PS 函数,但这足以用作工作示例。

$fileName = "foo"
$splitSize = 100MB

# need to sync .NET CurrentDirectory with PowerShell CurrentDirectory
# https://stackoverflow.com/questions/18862716/current-directory-from-a-dll-invoked-from-powershell-wrong
[Environment]::CurrentDirectory = Get-Location
# 4k is a fairly typical and 'safe' chunk size
# partial chunks are handled below
$bytes = New-Object byte[] 4096

$inFile = [System.IO.File]::OpenRead($fileName)

# track which output file we're up to
$fileCount = 0

# better to use functions but a flag is easier in a simple script
$finished = $false

while (!$finished) {
    $fileCount++
    $bytesToRead = $splitSize

    # Just like File::OpenWrite except CreateNew instead to prevent overwriting existing files
    $outFile = New-Object System.IO.FileStream "${fileName}_$fileCount",CreateNew,Write,None

    while ($bytesToRead) {
        # read up to 4k at a time, but no more than the remaining bytes in this split
        $bytesRead = $inFile.Read($bytes, 0, [Math]::Min($bytes.Length, $bytesToRead))

        # 0 bytes read means we've reached the end of the input file
        if (!$bytesRead) {
            $finished = $true
            break
        }

        $bytesToRead -= $bytesRead

        $outFile.Write($bytes, 0, $bytesRead)
    }

    # dispose closes the stream and releases locks
    $outFile.Dispose()
}

$inFile.Dispose()

相关内容