PowerShell - System.OutOfMemoryException

3

Eu quero Get-Content de um arquivo grande (1GB - 10GB) .txt (que tem apenas 1 linha!) e divido em vários arquivos com várias linhas, mas sempre que tento, acabo com uma System.OutOfMemoryException .

Claro que procurei uma solução, mas todas as soluções que encontrei foram ler um arquivo linha por linha, o que é meio difícil quando o arquivo tem apenas 1 linha.

Embora o PowerShell consiga até 4 GB de RAM ao carregar um arquivo de 1 GB, o problema não está conectado à minha RAM, pois tenho um total de 16 GB e, mesmo com um jogo em segundo plano, o pico é de aproximadamente 60%.

Estou usando o Windows 10 com PowerShell 5.1 (64 bits) e meu MaxMemoryPerShellMB está definido com o valor padrão de 2147483647 .

Este é o script que eu escrevi e estou usando, funciona bem com um tamanho de arquivo de, e. 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
    }
}

Se houver uma solução mais fácil que não use Get-Content , também aceito isso com prazer. Realmente não importa muito como eu alcanço o resultado, desde que seja possível fazê-lo com todas as máquinas atualizadas do Windows e nenhum software extra. Se isso, no entanto, não deveria ser possível, eu também consideraria outras soluções.

    
por FatalBulletHit 15.02.2018 / 00:01

1 resposta

5

Ler arquivos grandes na memória simplesmente para dividi-los, embora seja fácil, nunca será o método mais eficiente, e você encontrará os limites de memória em algum lugar .

Isto é ainda mais evidente aqui porque Get-Content trabalha em strings - e, como você mencionou nos comentários, você está lidando com arquivos binários.

O .NET (e, portanto, o PowerShell) armazena todas as cadeias de caracteres na memória como unidades de código UTF-16. Isto significa que cada unidade de código ocupa 2 bytes na memória.

Ocorre que uma única string do .NET pode armazenar apenas unidades de código (2 ^ 31 - 1), já que o comprimento de uma string é rastreado por um Int32 (mesmo em versões de 64 bits). Multiplique isso por 2 e uma única string do .NET pode (teoricamente) usar cerca de 4 GB.

Get-Content armazenará todas as linhas em sua própria sequência. Se você tiver uma única linha com > 2 bilhões de caracteres ... é provável que você esteja recebendo esse erro apesar de ter memória "suficiente".

Como alternativa, pode ser porque há um limite de 2 GB para qualquer objeto , a menos que tamanhos maiores sejam explicitamente ativados ( eles são para o PowerShell?). Sua OOM de 4 GB poderia também ser porque há duas cópias / buffers em volta, pois Get-Content tenta encontrar uma quebra de linha para dividir.

A solução, claro, é trabalhar com bytes e não com caracteres (strings).

Se você quiser evitar programas de terceiros, a melhor maneira de fazer isso é entrar nos métodos .NET. Isso é feito mais facilmente com uma linguagem completa como o C # (que pode ser incorporada ao PowerShell), mas é possível fazer apenas com o PS.

A ideia é que você queira trabalhar com matrizes de bytes, não com fluxos de texto. Existem duas maneiras de fazer isso:

  • Use [System.IO.File]::ReadAllBytes e [System.IO.File]::WriteAllBytes . Isso é muito fácil, e melhor que strings (sem conversão, sem uso de memória 2x), mas ainda terá problemas com arquivos muito grandes - digamos que você queira processar arquivos de 100 GB?

  • Use fluxos de arquivos e leia / escreva em blocos menores. Isso requer um pouco mais de matemática, já que você precisa manter o controle de sua posição, mas evita ler o arquivo inteiro na memória de uma só vez. Essa provavelmente será a abordagem mais rápida: a alocação de objetos muito grandes provavelmente superará a sobrecarga de várias leituras.

Assim, você lê trechos de tamanho razoável (atualmente, o mínimo é de 4kB por vez) e os copia para o arquivo de saída um trecho de cada vez, em vez de ler todo o arquivo na memória e dividi-lo. Você pode querer ajustar o tamanho para cima, por exemplo 8kB, 16kB, 32kB, etc., se você precisar extrair até a última gota de desempenho - mas você precisaria fazer benchmark para encontrar o tamanho ideal, pois alguns tamanhos maiores são mais lentos.

Um exemplo de script é o seguinte. Para reusabilidade, ele deve ser convertido em um cmdlet ou pelo menos uma função PS, mas isso é suficiente para servir como um exemplo prático.

$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()
    
por 15.02.2018 / 01:52