PowerShell: импорт 16 МБ CSV в переменную PowerShell создает более 600 МБ памяти PowerShell. - PullRequest
5 голосов
/ 22 февраля 2020

Я пытаюсь понять, почему память PowerShell так сильно раздувается, когда я импортирую файл размером ~ 16 МБ в качестве переменной. Я могу понять, что вокруг этой переменной есть дополнительная структура памяти, но я просто пытаюсь понять, почему она ТАК высока. Вот что я делаю ниже - просто упрощенный фрагмент c другого скрипта, который может запустить каждый.

Примечания / Вопросы

  1. Не жалуюсь, пытаясь понять, почему так много использования и есть ли лучший способ сделать это или управлять памятью более эффективно, чтобы уважать система, на которой я работаю.
  2. Такое же поведение наблюдается в PowerShell 5.1 и PowerShell 7, RC3 только что выпущен. Я не думаю, что это ошибка, просто еще одна возможность для меня, чтобы узнать больше.
  3. Моя общая цель с этим состоит в том, чтобы запустить foreach l oop, чтобы проверить другой гораздо меньший массив для этого массива на совпадения или не хватает.

Мой тестовый код

Invoke-WebRequest -uri "http://s3.amazonaws.com/alexa-static/top-1m.csv.zip" -OutFile C:\top-1m.csv.zip

Expand-Archive -Path C:\top-1m.csv.zip -DestinationPath C:\top-1m.csv

$alexaTopMillion = Import-Csv -Path C:\top-1m.csv

Всем, кто ответит на это: Спасибо, что уделили мне время и помогли мне узнать больше каждый день!

Ответы [ 2 ]

8 голосов
/ 22 февраля 2020

Как правило, говоря, совет iRon в комментарии к вопросу заслуживает внимания (конкретный вопрос c рассматривается в следующем разделе): 1005 *

Чтобы поддерживать низкий уровень использования памяти, используйте потоковое объектов в конвейере , а не , сначала собирая их в памяти - если возможно:

То есть вместо этого:

# !! Collects ALL objects in memory, as an array.
$rows = Import-Csv in.csv
foreach ($row in $rows) { ... }

сделайте это:

# Process objects ONE BY ONE.
# As long as you stream to a *file* or some other output stream
# (as opposed to assigning to a *variable*), memory use should remain constant,
# except for temporarily held memory awaiting garbage collection.
Import-Csv in.csv | ForEach-Object { ... } # pipe to Export-Csv, for instance

Однако, даже тогда, казалось бы, может закончиться памяти с очень большими файлами - см. этот вопрос - возможно , связанный с накоплением памяти из ненужных объектов, которые еще не были мусором -collected; поэтому, периодический вызов [GC]::Collect() в блоке сценария ForEach-Object может решить проблему.


Если вам do необходимо собрать все объекты, выведенные с помощью Import-Csv в памяти за один раз :

Использование неправильного памяти, которое вы наблюдаете, происходит из того, как [pscustomobject] экземпляров (тип вывода Import-Csv) ) реализованы , как обсуждалось в этой проблеме GitHub (выделение добавлено):

Скорее всего, нехватка памяти возникает из-за стоимости PSNoteProperty [то есть как [pscustomobject] свойства реализованы]. Каждый PSNoteProperty имеет накладные расходы в 48 байтов , поэтому , когда вы просто сохраняете несколько байтов на свойство, это становится огромным .

Та же проблема предлагает обходной путь для уменьшения потребления памяти (как также показано в ответ Васифа Хасана ):

  • Считайте первую строку CVS и динамически создайте пользовательский класс , представляющий строки с использованием Invoke-Expression.

    • Примечание. Хотя его использование здесь безопасно, обычно следует избегать Invoke-Expression .

    • Если вы заранее знаете структуру столбцов, вы можете создать пользовательский class обычным способом, который также позволяет использовать соответствующие типы данных для свойств (которые в противном случае все строки по умолчанию); например, определение соответствующих свойств как [int] (System.Int32) еще больше уменьшает потребление памяти.

  • Передача Import-Csv в ForEach-Object вызов, который преобразует каждый [pscustomobject], созданный для экземпляра динамически создаваемого класса, который более эффективно хранит данные.

Примечание: этот обходной путь обходится в дорогостоящий , значительно уменьшенный скорость выполнения .

$csvFile = 'C:\top-1m.csv'

# Dynamically define a custom class derived from the *first* row
# read from the CSV file.
# Note: While this is a legitimate use of Invoke-Expression, 
#       it should generally be avoided.
"class CsvRow { 
 $((Import-Csv $csvFile | Select-Object -first 1).psobject.properties.Name -replace '^', '[string] $$' -join ";") 
}" | Invoke-Expression

# Import all rows and convert them from [pscustomobject] instances 
# to [CsvRow] instances to reduce memory consumption.
# Note: Casting the Import-Csv call directly to [CsvRow[]] would be noticeably
#       faster, but increases *temporary* memory pressure substantially.
$alexaTopMillion = Import-Csv $csvFile | ForEach-Object { [CsvRow] $_ }

В долгосрочной перспективе лучшее решение, которое также будет быстрее , - Import-Csv поддерживает вывод проанализированных строк с данным типом вывода , скажем, через параметр -OutputType, как предложено в этом выпуске GitHub .
Если это представляет интерес покажите свою поддержку этому предложению.


Тесты использования памяти:

Следующий код сравнивает использование памяти с обычным Import-Csv импортом (массивом [pscustomobject] с ) к обходному пути (массив экземпляров пользовательского класса).

Измерение не является точным, поскольку рабочая память процесса PowerShell просто запрашивается, что может показать влияние фоновых операций, но дает приблизительное представление о том, насколько меньше памяти требуется пользовательскому классу.

Пример вывода, который показывает, что обходной путь пользовательского класса требует только около одной 5-й памяти с образцом входного файла CSV с 10 столбцами и примерно 166 000 строк, используемых ниже - коэффициент c зависит от количества входных данных строки и столбцы:

MB Used Command
------- -------
 384.50  # normal import…
  80.48  # import via custom class…

Код теста:

# Create a sample CSV file with 10 columns about 16 MB in size.
$tempCsvFile = [IO.Path]::GetTempFileName()
('"Col1","Col2","Col3","Col4","Col5","Col6","Col7","Col8","Col9","Col10"' + "`n") | Set-Content -NoNewline $tempCsvFile
('"Col1Val","Col2Val","Col3Val","Col4Val","Col5Val","Col6Val","Col7Val","Col8Val","Col9Val","Col10Val"' + "`n") * 1.662e5 |
  Add-Content $tempCsvFile

try {

  { # normal import
    $all = Import-Csv $tempCsvFile
  },
  { # import via custom class
    "class CsvRow {
      $((Import-Csv $tempCsvFile | Select-Object -first 1).psobject.properties.Name -replace '^', '[string] $$' -join ";")
    }" | Invoke-Expression
    $all = Import-Csv $tempCsvFile | ForEach-Object { [CsvRow] $_ }
  } | ForEach-Object {
    [gc]::Collect(); [gc]::WaitForPendingFinalizers() # garbage-collect first.
    $before = (Get-Process -Id $PID).WorkingSet64
    # Execute the command.
    & $_
    # Measure memory consumption and output the result.
    [pscustomobject] @{
      'MB Used' = ('{0,4:N2}' -f (((Get-Process -Id $PID).WorkingSet64 - $before) / 1mb)).PadLeft(7)
      Command = $_
    }
  }

} finally {
  Remove-Item $tempCsvFile
}
2 голосов
/ 22 февраля 2020

Вы можете создать тип для каждого элемента, как описано здесь https://github.com/PowerShell/PowerShell/issues/7603

Import-Csv "C:\top-1m.csv" | Select-Object -first 1 | ForEach {$_.psobject.properties.name} | Join-String -Separator "`r`n" -OutputPrefix "class MyCsv {`r`n" -OutputSuffix "`n}" -Property {"`t`$$_"}  | Invoke-Expression
Import-Csv "C:\top-1m.csv" | Foreach {[MyCsv]$_} | Export-Csv "C:\alexa_top.csv"

Это гораздо более эффективно. Вы можете измерить время с помощью Measure-Command.

Если вы используете Get-Content, он очень-очень медленный. Параметр Raw улучшает скорость. Но давление памяти становится высоким.

Даже параметр ReadCount устанавливает строки для чтения для каждого процесса для чтения. Это даже быстрее, чем параметр Raw.

Его можно даже прочитать с помощью оператора Switch, например:

Switch -File "Path" {default {$_}}

Это еще быстрее! Но, к сожалению, он даже использовал больше памяти.

...