PowerShell:将 16MB CSV 导入 PowerShell 变量会产生 >600MB 的 PowerShell 内存使用

PowerShell: Importing 16MB CSV Into PowerShell Variable Creates >600MB's of PowerShell Memory Usage

我试图理解为什么当我导入一个大约 16MB 的文件作为变量时,PowerShell 的内存膨胀如此之大。我可以理解该变量周围有额外的内存结构,但我只是想了解为什么它那么高。这是我在下面所做的 - 只是一个任何人都可以 运行 的另一个脚本的简化片段。

Notes/Questions

  1. 不抱怨,试图理解为什么使用如此之多,是否有更好的方法来执行此操作或更有效地管理内存以尊重我正在使用的系统。
  2. PowerShell 5.1 和刚刚发布的 RC3 的 PowerShell 7 中也会出现同样的行为。我不认为这是一个错误,只是我学习更多的另一个机会。
  3. 我的总体 objective 是 运行 一个 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

致所有回答此问题的人:感谢您抽出时间帮助我每天学习更多!

您可以按照此处所述为每个项目生成类型 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 {$_}}

更快!但是后悔竟然占用了更多的内存

一般来说iRon在对该问题的评论中的建议值得关注(具体问题将在本节后面的部分中解决):

To keep memory usage low, use streaming of objects in the pipeline rather than collecting them in memory first - if feasible.

也就是说,而不是这样做:

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

但是,即便如此,您似乎 可以 运行 内存不足 非常大 文件 - 参见 - 可能 与从不再需要的尚未被垃圾收集的对象中积累内存有关;因此,在ForEach-Object脚本块中定期调用[GC]::Collect()可能解决问题。


如果你需要一次性收集Import-Csv输出的所有对象在内存中:

您观察到的异常内存使用来自[pscustomobject]实例(Import-Csv的输出类型)的实现方式,如 GitHub issue #7603 中所讨论(强调已添加):

The memory pressure most likely comes from the cost of PSNoteProperty [which is how [pscustomobject] properties are implemented]. Each PSNoteProperty has an overhead of 48 bytes, so when you just store a few bytes per property, that becomes massive.

同一问题提出了 解决方法 以减少内存消耗(如 中所示):

  • 读取第一个 CVS 行并使用 Invoke-Expression.[=43 动态创建代表行的 自定义 class =]

    • 注意:虽然在这里使用它是安全的,Invoke-Expression should generally be avoided.

    • 如果您事先知道列结构,则可以按常规方式创建自定义 class,这还允许您为属性使用适当的数据类型(否则都是字符串默认);例如,将适当的属性定义为 [int] (System.Int32) 可进一步减少内存消耗。

  • 管道 Import-CsvForEach-Object 调用,将创建的每个 [pscustomobject] 转换为动态创建的实例 class,它存储数据更高效。

注意:此 解决方法的代价是 执行速度大大降低 .

$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 issue #8862.
中提出的 如果您对此感兴趣,请在此表示您对提案的支持。


内存使用基准:

以下代码将内存使用与正常 Import-Csv 导入([pscustomobject]s 数组)与解决方法(自定义数组-class 实例)进行比较。

测量不准确,因为只是查询 PowerShell 的进程工作内存,这可以显示后台活动的影响,但它粗略地了解使用自定义 class需要。

示例输出,显示自定义-class 解决方法仅需要大约 五分之一 的内存,示例 10 列 CSV 输入文件大约有 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
}