Вообще говоря, совет 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
$alexaTopMillion
) или использовать квадратные скобки ((...)
). Но направьте ввод в свой процесс и сразу же направьте его на вывод:Import-Csv -Path C:\top-1m.csv | ForEach-Object {...} | Export-Csv -Path C:\output.csv
- person iRon   schedule 22.02.2020