首页 > 解决方案 > 从数组的属性中获取唯一索引项的最快方法

问题描述

制作一个这样的数组,代表我正在寻找的内容:

$array = @(1..50000).foreach{[PSCustomObject]@{Index=$PSItem;Property1='Hello!';Property2=(Get-Random)}}

使用索引属性“43122”获取项目的最快方法是什么?

我有一些想法,但我觉得必须有一个更快的方法:

哪里管道

measure-command {$array | where-object index -eq 43122} | % totalmilliseconds
420.3766

哪里方法

measure-command {$array.where{$_ -eq 43122}} | % totalmilliseconds
155.1342

首先制作一个哈希表并查询“索引”结果。一开始很慢,但随后的查找速度更快。

measure-command {$ht = @{};$array.foreach{$ht[$PSItem.index] = $psitem}} | % totalmilliseconds
124.0821

measure-command {$ht.43122} | % totalmilliseconds
3.4076

有没有比先构建哈希表更快的方法?也许是一种不同的 .NET 数组类型,比如某种特殊的索引列表,我最初可以将它存储在其中,然后运行一个方法来根据唯一属性提取项目?

标签: powershell

解决方案


部分归功于 PowerShell 能够调用.Net方法这一事实,它提供了一些过滤对象的可能性。在 stackoverflow 上,您会发现很多 (PowerShell) 问题和答案,用于衡量特定解压命令或cmdlet的性能。这通常会留下错误的印象,因为一个完整的 (PowerShell) 解决方案的性能应该优于其部分的总和。每个命令都取决于预期的输入和输出。尤其是在使用 PowerShell 管道时,命令 (cmdlet) 会与之前的命令和随后的命令进行交互。因此,重要的是要着眼大局并了解每个命令如何以及在何处获得其性能。
这意味着我无法告诉您应该选择哪个命令,但是通过对下面列出的命令和概念的更好理解,我希望您能够更好地为您的具体解决方案找到“最快的方法”。

[Linq.Enumerable]::Where

语言集成查询 (LINQ)通常(不)被认为是在 PowerShell 中过滤对象的快速解决方案(另请参阅高性能 PowerShell 与 LINQ):

(Measure-Command {
    $Result = [Linq.Enumerable]::Where($array, [Func[object,bool]] { param($Item); return $Item.Index -eq 43122 })
}).totalmilliseconds
4.0715

刚刚结束4ms!,没有其他方法可以击败它......
但是在得出任何结论之前LINQ击败任何其他方法 100 倍或更多,您应该牢记以下几点。当您仅查看活动本身的性能时,在衡量 LINQ 查询的性能时有两个陷阱:

  • LINQ 有一个很大的缓存,这意味着您应该重新启动一个新的 PowerShell 会话来测量实际结果(或者,如果您经常想重用查询,则不这样做)。重新启动 PowerShell 会话后,您会发现启动 LINQ 查询需要大约 6 倍的时间。
  • 但更重要的是,LINQ 执行惰性求值(也称为延迟执行)。这意味着除了定义应该做什么之外,实际上还没有做任何事情。这实际上表明您是否要访问以下属性之一$Result

(Measure-Command {
    $Result.Property1
}).totalmilliseconds
532.366

通常需要15ms检索单个对象的属性:

$Item = [PSCustomObject]@{Index=1; Property1='Hello!'; Property2=(Get-Random)}
(Measure-Command {
    $Item.Property1
}).totalmilliseconds
15.3708

最重要的是,您需要实例化结果以正确测量 LINQ 查询的性能(为此,我们只需在测量中检索返回对象的属性之一):

(Measure-Command {
    $Result = ([Linq.Enumerable]::Where($array, [Func[object,bool]] { param($Item); return $Item.Index -eq 43122 })).Property1
}).totalmilliseconds
570.5087

(这仍然很快。)

HashTable

哈希表通常很快,因为它们基于二进制搜索算法,这意味着您最多需要猜测ln 50000 / ln 2 = 16 times才能找到您的对象。然而,HashTabe为单个查找构建一个有点过头了。但是如果你控制对象列表的构造,你可能会在旅途中构造哈希表:

(Measure-Command {
    $ht = @{}
    $array = @(1..50000).foreach{$ht[$PSItem] = [PSCustomObject]@{Index=$PSItem;Property1='Hello!';Property2=(Get-Random)}}
    $ht.43122
}).totalmilliseconds
3415.1196

与:

(Measure-Command {
    $array = @(1..50000).foreach{[PSCustomObject]@{Index=$PSItem;Property1='Hello!';Property2=(Get-Random)}}
    $ht = @{}; $array.foreach{$ht[$PSItem.index] = $psitem}
    $ht.43122
}).totalmilliseconds
3969.6451

Where-ObjectcmdletWhere方法

您可能已经得出结论,该Where方法的出现Where-Object速度大约是cmdlet的两倍:

Where-Object小命令

(Measure-Command {
    $Result = $Array | Where-Object index -eq 43122
}).totalmilliseconds
721.545

Where方法:

(Measure-Command {
    $Result = $Array.Where{$_ -eq 43122}
}).totalmilliseconds
319.0967

原因是该命令要求您将整个数组加载到内存中,而cmdletWhere实际上不需要。Where-Object如果数据已经在内存中(例如,通过将其分配给变量$array = ...),这没什么大不了的,但这本身可能实际上是一个缺点:除了它会消耗内存之外,您必须等到所有对象都被接收到之后才可以开始过滤...

不要低估 PowerShell cmdlet 的强大功能,例如Where-Object将解决方案作为一个整体与管道结合使用。如上所示,如果您只衡量特定操作,您可能会发现这些 cmdlet 很慢,但如果您衡量整个端到端解决方案,您可能会发现没有太大差异,而且 cmdlet 甚至可能优于其他技术的方法。在 LINQ 查询非常被动的地方,PowerShell cmdlet 非常主动。
一般来说,如果您的输入尚未在内存中并通过管道提供,您应该尝试继续在该管道上构建并通过避免变量分配( $array = ...)和使用括号 ( (...)) 来避免以任何方式停止它:

假设您的对象来自较慢的输入,在这种情况下,所有其他解决方案都需要等待最后一个对象能够开始过滤,其中Where-Object已经过滤了大部分对象,并且一旦找到它,不确定地传递给下一个 cmdlet...

例如,假设数据来自csv文件而不是内存......

$Array | Export-Csv .\Test.csv

Where-Object小命令

(Measure-Command {
    Import-Csv -Path .\Test.csv | Where-Object index -eq 43122 | Export-Csv -Path .\Result.csv
}).totalmilliseconds
717.8306

Where方法:

(Measure-Command {
    $Array = Import-Csv -Path .\Test.csv
    Export-Csv -Path .\Result.csv -InputObject $Array.Where{$_ -eq 43122}
}).totalmilliseconds
747.3657

这只是一个测试示例,但在大多数情况下,数据不能立即在内存中可用Where-Object 流似乎通常比使用 Where 方法更快
此外,Where如果您的文件(对象列表)大小超过可用物理内存,该方法会使用更多内存,这可能会使性能更差。(另请参阅:可以在 PowerShell 中简化以下嵌套的 foreach 循环吗?)。

ForEach-Objectcmdlet vsForEach方法vsForEach命令

您可能会考虑遍历所有对象并将它们与 语句进行比较,而不是使用Where-Objectcmdlet 或方法。在深入研究这种方法之前,值得一提的是,比较运算符本身已经遍历了左参数,引用:WhereIf

当运算符的输入是标量值时,比较运算符返回布尔值。当输入是值的集合时,比较运算符会返回任何匹配的值。如果集合中没有匹配项,则比较运算符返回一个空数组。

这意味着如果您只想知道具有特定属性的对象是否存在而不关心对象本身,您可能只是简单地比较特定属性集合:

(Measure-Command {
    If ($Array.Index -eq 43122) {'Found object with the specific property value'}
}).totalmilliseconds
55.3483

对于ForEach-Objectcmdlet 和ForEach方法,您会看到该方法比使用它们的对应项(Where-Objectcmdlet 和Where方法)花费的时间稍长一些,因为嵌入式比较的开销更大:

直接从内存中:
ForEach-Objectcmdlet

(Measure-Command {
    $Result = $Array | ForEach-Object {If ($_.index -eq 43122) {$_}}
}).totalmilliseconds
1031.1599

ForEach方法:

(Measure-Command {
    $Result = $Array.ForEach{If ($_.index -eq 43122) {$_}}
}).totalmilliseconds
781.6769

从磁盘流式传输:
ForEach-Objectcmdlet

(Measure-Command {
    Import-Csv -Path .\Test.csv |
    ForEach-Object {If ($_.index -eq 43122) {$_}} |
    Export-Csv -Path .\Result.csv
}).totalmilliseconds
1978.4703

ForEach方法:

(Measure-Command {
    $Array = Import-Csv -Path .\Test.csv
    Export-Csv -Path .\Result.csv -InputObject $Array.ForEach{If ($_.index -eq 43122) {$_}}
}).totalmilliseconds
1447.3628

ForEachcommand 但即使使用嵌入式比较,当内存中已经可用 时,该ForEach 命令看起来也接近使用该Where方法的性能:$Array

直接凭记忆:

(Measure-Command {
    $Result = $Null
    ForEach ($Item in $Array) {
        If ($Item.index -eq 43122) {$Result = $Item}
    }
}).totalmilliseconds
382.6731

从磁盘流式传输:

(Measure-Command {
    $Result = $Null
    $Array = Import-Csv -Path .\Test.csv
    ForEach ($Item in $Array) {
        If ($item.index -eq 43122) {$Result = $Item}
    }
    Export-Csv -Path .\Result.csv -InputObject $Result
}).totalmilliseconds
1078.3495

ForEach但是,如果您只查找一个(或第一个)事件,则使用该命令可能还有另一个优点:Break一旦找到对象,您就可以退出循环,然后简单地跳过数组迭代的其余部分。换句话说,如果该项目出现在最后,则可能没有太大区别,但如果它出现在开头,您将赢得很多。为了平衡这一点,我采用了平均索引 ( 25000) 进行搜索:

(Measure-Command {
    $Result = $Null
    ForEach ($Item in $Array) {
        If ($item.index -eq 25000) {$Result = $Item; Break}
    }
}).totalmilliseconds
138.029

请注意,您不能使用cmdlet 和方法的Break语句,请参阅:How to exit from ForEach-Object in PowerShellForEach-ObjectForEach

结论

纯粹看测试的命令并做出一些假设,例如:

  • 输入不是瓶颈($Array已经驻留在内存中)
  • 输出不是瓶颈($Result实际没有使用)
  • 您只需要一次(第一次)出现
  • 在迭代之前、之后和之中没有其他事情可做

使用该ForEach 命令并简单地比较每个索引属性直到找到对象,这似乎是该问题的给定/假设边界中最快的方法,但如开头所述;要确定最适合您的用例的方法,您应该了解自己在做什么并查看整个解决方案,而不仅仅是一部分。


推荐阅读