PowerShell: импорт 16 МБ CSV в переменную PowerShell создает >600 МБ использования памяти PowerShell

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

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

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

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

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

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


person Dave Spatz    schedule 22.02.2020    source источник
comment
Вам необходимо передавать свои объекты с помощью конвейера PowerShell, что в основном означает не присваивать весь объект переменной ($alexaTopMillion) или использовать квадратные скобки ((...)). Но направьте ввод в свой процесс и сразу же направьте его на вывод: Import-Csv -Path C:\top-1m.csv | ForEach-Object {...} | Export-Csv -Path C:\output.csv   -  person iRon    schedule 22.02.2020


Ответы (2)


Вообще говоря, совет iRon в комментарии к вопросу заслуживает внимания (конкретные вопрос рассматривается в следующем разделе):

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

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

# !! 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

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


Если вам действительно нужно собрать все объекты, выводимые 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.< br> Если это вас интересует, продемонстрируйте свою поддержку предложения там.


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

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

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

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

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
}
person mklement0    schedule 22.02.2020
comment
Ух ты! Я в шоке от всех ответов здесь. Спасибо, что помогли мне с этим! - person Dave Spatz; 23.02.2020
comment
Рад слышать, что это было полезно, @DaveSpatz. Это был хороший вопрос, и в настоящее время нет официального источника этой информации. Я призываю вас поддержать запрос связанной функции, поскольку встроенный в решении, которое эффективно использует память и быстро, безусловно, требуется. - person mklement0; 23.02.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 устанавливает количество строк для чтения для каждого процесса. Это даже быстрее, чем использование исходного параметра.

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

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

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

person programmer365    schedule 22.02.2020
comment
Как и в моих комментариях к вопросу. Вы не должны не назначать его переменной ($alexaTopMillion = ...), которая будет забивать конвейер и накапливать все в памяти. Вместо этого вы должны напрямую передать его следующему командлету (Export-Csv), чтобы промежуточно освободить объекты из памяти. См. также: Реализовать середину конвейера - person iRon; 22.02.2020
comment
Спасибо @iRon. Я обновил свой ответ. Я также включил, как увеличить скорость чтения файлов. (Тем не менее они требуют большой памяти....) - person programmer365; 22.02.2020
comment
Обходной путь помогает уменьшить потребление памяти, но он значительно замедляет выполнение команды. ОП беспокоит использование памяти по отношению к Import-Csv, тогда как в остальной части вашего ответа обсуждается скорость выполнения с обычным текстом файлы. Здесь -Raw намного быстрее (даже если вы используете -ReadCount 0 для чтения в одиночный массив), но вы получаете одну многострочную строку. Вы также можете сделать поток switch в конвейере - просто оберните весь оператор в & { ... }. - person mklement0; 22.02.2020