【问题标题】:Powershell 2 and .NET: Optimize for extremely large hash tables?Powershell 2 和 .NET:针对超大哈希表进行优化?
【发布时间】:2011-11-23 07:13:57
【问题描述】:

我正在涉足 Powershell 并且对 .NET 完全陌生。

我正在运行一个以空哈希表开头的 PS 脚本。哈希表将增长到至少 15,000 到 20,000 个条目。哈希表的键是字符串形式的电子邮件地址,值是布尔值。 (我只需要跟踪我是否看过电子邮件地址。)

到目前为止,我一直在一次增加一个哈希表条目。我检查以确保键值对不存在(在这种情况下 PS 会出错),然后添加该对。

这是我们正在讨论的我的代码部分:

...
    if ($ALL_AD_CONTACTS[$emailString] -ne $true) {
      $ALL_AD_CONTACTS += @{$emailString = $true}
    }
...

我想知道从 PowerShell 或 .NET 的角度来看,如果您提前知道该哈希表将是巨大的(例如 15,000 到 20,000 个或更多条目),是否可以做任何事情来优化此哈希表的性能。

谢谢!

【问题讨论】:

  • 哈希表是正确的结构吗?基本上,如果它在哈希表中,它的值为真,不是吗?
  • 正确。键是电子邮件地址,值只是 $true。为了确定任意电子邮件地址是否在该集合中,有什么更有效的方法可以记住 15,000 个电子邮件地址?我认为哈希表是常数时间查找而不是数组。
  • 我对 PowerShell 中的可用功能不够熟悉。在 .NET 中,我会选择 List 而不测量性能。在查找/插入时创建哈希需要时间,而且您也不需要存储“值”,因此这是不需要的操作。我不知道 List 的 O 因子,但如果性能至关重要,我会对其进行衡量。

标签: powershell hashtable powershell-2.0


【解决方案1】:

我使用Measure-Command 执行了一些基本测试,使用了一组 20 000 个random words

各个结果如下所示,但总的来说,通过首先分配具有单个条目的新哈希表来添加到一个哈希表似乎是非常低效的 :) 尽管选项 2 到 5 之间有一些小的效率提升,但总的来说他们的表现都差不多。

如果要我选择,我可能会倾向于选项 5,因为它很简单(每个字符串只需一个 Add 调用),但我测试的所有替代方案似乎都是可行的。

$chars = [char[]]('a'[0]..'z'[0])
$words = 1..20KB | foreach {
  $count = Get-Random -Minimum 15 -Maximum 35
  -join (Get-Random $chars -Count $count)
}

# 1) Original, adding to hashtable with "+=".
#     TotalSeconds: ~800
Measure-Command {
  $h = @{}
  $words | foreach { if( $h[$_] -ne $true ) { $h += @{ $_ = $true } } }
}

# 2) Using sharding among sixteen hashtables.
#     TotalSeconds: ~3
Measure-Command {
  [hashtable[]]$hs = 1..16 | foreach { @{} }
  $words | foreach {
    $h = $hs[$_.GetHashCode() % 16]
    if( -not $h.ContainsKey( $_ ) ) { $h.Add( $_, $null ) }
  }
}

# 3) Using ContainsKey and Add on a single hashtable.
#     TotalSeconds: ~3
Measure-Command {
  $h = @{}
  $words | foreach { if( -not $h.ContainsKey( $_ ) ) { $h.Add( $_, $null ) } }
}

# 4) Using ContainsKey and Add on a hashtable constructed with capacity.
#     TotalSeconds: ~3
Measure-Command {
  $h = New-Object Collections.Hashtable( 21KB )
  $words | foreach { if( -not $h.ContainsKey( $_ ) ) { $h.Add( $_, $null ) } }
}

# 5) Using HashSet<string> and Add.
#     TotalSeconds: ~3
Measure-Command {
  $h = New-Object Collections.Generic.HashSet[string]
  $words | foreach { $null = $h.Add( $_ ) }
}

【讨论】:

    【解决方案2】:

    所以几周后,我无法想出完美的解决方案。 Google 的一位朋友建议将散列分成几个较小的散列。他建议每次我去查找一个密钥时,我都会有几次未命中,直到找到正确的“存储桶”,但他说当碰撞算法运行时,读取惩罚不会像写入惩罚那么糟糕将条目插入(已经很庞大的)哈希表中。

    我接受了这个想法并将其更进一步。我将散列分成 16 个较小的桶。在将电子邮件地址作为键插入数据结构时,我实际上首先计算电子邮件地址本身的哈希值,然后执行 mod 16 运算以获得 0 到 15 之间的一致值。然后我使用该计算值作为“桶”号。

    因此,我实际上有一个 16 元素数组,而不是使用一个巨大的哈希,其元素是电子邮件地址的哈希表。

    使用拆分哈希表存储桶构建我的 20,000 多个电子邮件地址的“主列表”的内存表示所需的总速度现在大约快 1,000%。 (快 10 倍)。

    访问散列中的所有数据没有明显的速度延迟。这是迄今为止我能想到的最好的解决方案。它有点难看,但性能提升不言而喻。

    【讨论】:

    • Bucket 方法是正确的方法,它在其他语言中得到了有效使用。尽管哈希表查找具有对数复杂性,但在大循环中访问时,减小存储桶大小仍然有很多好处。至于哈希表的+=,在PowerShell中非常慢,因为整个集合都被复制了,所以只需使用直接赋值:$hashtable[$key] = $value
    • 您是否有任何链接/参考来支持桶方法确实表现更好的想法?正如@emperor-xlii 指出的那样,运行时间没有真正的差异(在我的测试中,它的性能差了 2 倍)。我的意思是 - GetHashCode() % somefactor 不是 Hashtables 在内部已经做了什么吗?您预计多久会发生一次碰撞?最重要的是:您优化什么 - 写入速度或读取速度?
    【解决方案3】:

    您将花费大量 CPU 时间重新分配 Hashtable 中的内部“数组”。你试过.NET constructor for Hashtable that takes a capacity吗?

    $t = New-Object Hashtable 20000
    ...
    if (!($t.ContainsKey($emailString))) { 
        $t.Add($emailString, $emailString) 
    }
    

    我的版本使用相同的 $emailString 作为键和值,没有 .NET 将 $true 装箱到 [object] 只是作为占位符。非空字符串将在 PowerShell 'if' 条件中计算为 $true,因此您检查的其他代码不应更改。您对 '+= @{...}' 的使用将是对性能敏感的 .NET 代码的一大禁忌。您可能只是使用“@{}”语法为每封电子邮件分配一个新的 Hashtable,这可能会浪费大量时间。

    您将非常大的集合分解为(相对较少)数量的较小集合的方法称为“分片”。即使您按 16 分片,您也应该使用具有容量的 Hashtable 构造函数。

    另外,@Larold 是对的,如果您不查找电子邮件地址,请使用“New-Object ArrayList 20000”创建一个预分配列表。

    此外,收藏品的增长速度非常快(每个“增长”的因子为 1.5 或 2)。这样做的效果是,您应该能够按顺序减少预分配的数量,并且如果每次“数据加载”时集合调整大小一次或两次,您可能不会注意到。我敢打赌,前 10 到 20 代的“增长”需要时间。

    【讨论】:

      猜你喜欢
      • 2012-01-15
      • 1970-01-01
      • 1970-01-01
      • 2017-12-22
      • 2014-07-05
      • 2013-05-17
      • 2012-05-16
      • 2022-01-21
      • 2021-08-22
      相关资源
      最近更新 更多