首先,如果您要使用Get-ChildItem,那么您应该传递-File 开关参数,以便[System.IO.DirectoryInfo] 实例永远不会进入管道。
其次,您没有将-Force 开关参数传递给Get-ChildItem,因此不会检索到该目录结构中的任何隐藏文件。
第三,请注意,您的代码正在检索 32 个最大的文件,而不是具有 32 个最大长度的文件。也就是说,如果文件 31、32 和 33 的长度都相同,则文件 33 将被任意排除在最终计数之外。如果这种区别对您很重要,您可以像这样重写您的代码...
$filesByLength = Get-ChildItem -File -Force -Recurse -Path 'C:\Temp\' |
Group-Object -AsHashTable -Property Length
$big32 = $filesByLength.Keys |
Sort-Object -Descending |
Select-Object -First 32 |
ForEach-Object -Process { $filesByLength[$_] } |
Measure-Object -Property Length -Sum
$filesByLength 是一个[Hashtable],它从一个长度映射到具有该长度的文件。 Keys 属性包含所有检索到的文件的所有唯一长度,因此我们获得了 32 个最大的键/长度,并使用每一个将具有该长度的所有文件发送到管道中。
最重要的是,对检索到的文件进行排序以找到最大的文件是有问题的,原因如下:
- 在所有输入数据都可用之前,无法开始排序,这意味着此时所有 140 万个
[System.IO.FileInfo] 实例都将存在于内存中。
- 我不确定
Sort-Object 是如何缓冲传入管道数据的,但我想它会是某种列表,每次需要更多容量时,它的大小都会翻倍,从而导致内存中的更多垃圾需要清理。
- 140 万个
[System.IO.FileInfo] 实例中的每一个都将被第二次访问以获取它们的Length 属性,同时也发生任何排序操作(取决于Sort-Object 使用的算法)。
既然我们只关心 140 万个文件中最大的 32 个文件/长度,那么如果我们只跟踪这 32 个而不是全部 140 万个会怎样?考虑一下我们是否只想找到单个最大的文件...
$largestFileLength = 0
$largestFile = $null
foreach ($file in Get-ChildItem -File -Force -Recurse -Path 'C:\Temp\')
{
# Track the largest length in a separate variable to avoid two comparisons...
# if ($largestFile -eq $null -or $file.Length -gt $largestFile.Length)
if ($file.Length -gt $largestFileLength)
{
$largestFileLength = $file.Length
$largestFile = $file
}
}
Write-Host -Message "The largest file is named ""$($largestFile.Name)"" and has length $largestFileLength."
与Get-ChildItem ... | Sort-Object -Property Length -Descending | Select-Object -First 1 相比,这具有以下优点:一次只有一个[FileInfo] 对象处于“运行中”,并且整个[System.IO.FileInfo]s 集仅被枚举一次。现在我们需要做的就是采用相同的方法,但从 1 个文件/长度的“槽”扩展到 32...
$basePath = 'C:\Temp\'
$lengthsToKeep = 32
$includeZeroLengthFiles = $false
$listType = 'System.Collections.Generic.List[System.IO.FileInfo]'
# A SortedDictionary[,] could be used instead to avoid having to fully enumerate the Keys
# property to find the new minimum length, but add/remove/retrieve performance is worse
$dictionaryType = "System.Collections.Generic.Dictionary[System.Int64, $listType]"
# Create a dictionary pre-sized to the maximum number of lengths to keep
$filesByLength = New-Object -TypeName $dictionaryType -ArgumentList $lengthsToKeep
# Cache the minimum length currently being kept
$minimumKeptLength = -1L
Get-ChildItem -File -Force -Recurse -Path $basePath |
ForEach-Object -Process {
if ($_.Length -gt 0 -or $includeZeroLengthFiles)
{
$list = $null
if ($filesByLength.TryGetValue($_.Length, [ref] $list))
{
# The current file's length is already being kept
# Add the current file to the existing list for this length
$list.Add($_)
}
else
{
# The current file's length is not being kept
if ($filesByLength.Count -lt $lengthsToKeep)
{
# There are still available slots to keep more lengths
$list = New-Object -TypeName $listType
# The current file's length will occupy an empty slot of kept lengths
}
elseif ($_.Length -gt $minimumKeptLength)
{
# There are no available slots to keep more lengths
# The current file's length is large enough to keep
# Get the list for the minimum length
$list = $filesByLength[$minimumKeptLength]
# Remove the minimum length to make room for the current length
$filesByLength.Remove($minimumKeptLength) |
Out-Null
# Reuse the list for the now-removed minimum length instead of allocating a new one
$list.Clear()
# The current file's length will occupy the newly-vacated slot of kept lengths
}
else
{
# There are no available slots to keep more lengths
# The current file's length is too small to keep
return
}
$list.Add($_)
$filesByLength.Add($_.Length, $list)
$minimumKeptLength = ($filesByLength.Keys | Measure-Object -Minimum).Minimum
}
}
}
# Unwrap the files in each by-length list
foreach ($list in $filesByLength.Values)
{
foreach ($file in $list)
{
$file
}
}
我采用了上述方法,即检索具有 32 个最大长度的文件。 [Dictionary[Int64, List[FileInfo]]] 用于跟踪这 32 个最大长度以及具有该长度的相应文件。对于每个输入文件,我们首先检查它的长度是否是迄今为止最大的,如果是,则将该文件添加到现有的List[FileInfo] 中以获得该长度。否则,如果字典中还有空间,我们可以无条件添加输入文件及其长度,或者如果输入文件至少大于最小跟踪长度,我们可以删除该最小长度并在其位置添加输入文件及其长度。一旦没有更多的输入文件,我们就会从[Dictionary[Int64, [List[FileInfo]]]] 中的所有[List[FileInfo]]s 中输出所有[FileInfo] 对象。
我运行了这个简单的基准测试模板...
1..5 |
ForEach-Object -Process {
[GC]::Collect()
return Measure-Command -Expression {
# Code to test
}
} | Measure-Object -Property 'TotalSeconds' -Minimum -Maximum -Average
...在 PowerShell 7.2 上针对我的 $Env:WinDir 目录(325,000 个文件),结果如下:
# Code to test |
Minimum |
Maximum |
Average |
Memory usage* |
Get-ChildItem -File -Force -Recurse -Path $Env:WinDir |
69.7240896 |
79.727841 |
72.81731518 |
+260 MB |
Get $Env:WinDir files with 32 largest lengths using -AsHashtable, Sort-Object
|
82.7488729 |
83.5245153 |
83.04068032 |
+1 GB |
Get $Env:WinDir files with 32 largest lengths using dictionary of by-length lists |
81.6003697 |
82.7035483 |
82.15654538 |
+235 MB |
* 在Task Manager → Details 选项卡 → Memory (active private working set) 列中观察到
我有点失望,我的解决方案仅比使用 [Hashtable] 的 Keys 的代码快约 1%,但可能使用已编译的 cmdlet 对文件进行分组,而不是对文件进行分组或排序,而是使用更多(解释)PowerShell 代码是一个洗牌。尽管如此,内存使用量的差异仍然很大,尽管我无法解释为什么 Get-ChildItem 调用简单地枚举所有文件最终会使用更多。