PowerShell - System.OutOfMemoryException

Fat*_*Hit 4 powershell memory-error windows-10

我想要Get-Content一个大(1GB - 10GB).txt文件(只有 1 行!)并将其拆分为多个包含多行的文件,但是每当我尝试时,我都会以System.OutOfMemoryException.

当然,我确实在寻找解决方案,但我找到的所有解决方案都是逐行读取文件,当文件只有 1 行时,这有点难以做到。

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

我将 Windows 10 与 PowerShell 5.1(64 位)一起使用,并且 myMaxMemoryPerShellMB设置为默认值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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Bob*_*Bob 5

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

这在这里更加明显,因为它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
# /sf/ask/1320390151/
[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()
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

5736 次

最近记录:

7 年,10 月 前