Powershell - 创建大量哈希表的最快方法

Powershell - Fastest way to create a very large array of hashtables

我正在尝试创建一个相当大的哈希表数组,其中大部分数据要么完全随机化,要么从列表中随机选取。

这是我的当前代码

    $ArrayData = @()
    $ArrayDataRows = 150000

    foreach ($i in 1..$ArrayDataRows) {

        $thisobject = [PSCustomObject] @{
            Number = $i
            Place = Get-Random -InputObject NJ, UT, NY, MI, PA, FL, AL, NM, CA, OK, TX, CO, AZ
            Color = Get-Random -InputObject red, yellow, blue, purple, green, white, black
            Zone = (Get-Random -InputObject $([char[]](65..90)) -Count 10) -join ""
            Group = Get-Random -InputObject @(1..20)
        }


        $ArrayData += $thisobject 
     }

但我注意到,它似乎效率不高。完成 150k 行总共需要 25 分钟。

我还有一些未在此处发布的其他代码,这些代码测量了每个实例花费的时间并估算了从它到它的前辈的平均值。最初,它会给我估计总时间为 450 秒,对于前 3k 个项目,每个实例的平均时间为 0.002,但后来它一直缓慢爬行到 0.016 或平均速度慢 8 倍。

如何优化 and/or 以提高效率,同时获得相同的数据?

[编辑 - 您不是在制作哈希表数组。您正在制作一个包含 PSCustomObject 个项目的数组。 [*咧嘴一笑*]]

标准数组是一个固定大小对象。查看 $ArrayData.IsFixedSize 以确认这一点。 [咧嘴一笑]

因此,当您在标准数组上使用 += 时,powershell 会创建一个新的、一项更大的数组,将旧的数组复制到新的数组中,最后添加新的项。当项目数量和大小为 "small" 时它很快,但随着 count/size 的增长它变得越来越慢 [越来越慢,越来越慢]。

有两种常见的解决方案...

  • 使用具有 .Add() 方法的集合类型
    ArrayList [已弃用] 和 Generic.List 是人们通常使用的。当您添加到第一个时,它会输出一个索引号,因此即使它没有被弃用,我也不会使用它。 [咧嘴一笑]
  • 使用输出流
    您可以使用 $Results = foreach ($Thing in $Collection) {Do-Stuff} 并且脚本块的输出将保存在 RAM 中,直到循环完成。然后一次性全部塞进$Results集合。

第二个是最快的。

如果您在构建后不需要更改集合的大小,则使用第二种方法。否则使用第一个。

作为速度示例,您的代码 [包含 15,000 个项目] 在我的系统上运行时间为 39 秒。使用 "send to output" 技术需要 24 秒。

请记住,随着阵列变大,减速会继续恶化。我不愿意等待 150k 次迭代。

这是我的演示代码...

$ArrayDataRows = 15e3
$PlaceList = 'NJ, UT, NY, MI, PA, FL, AL, NM, CA, OK, TX, CO, AZ'.Split(',').Trim()
$ColorList = 'red, yellow, blue, purple, green, white, black'.Split(',').Trim()
$UC_LetterList = [char[]](65..90)
$GroupList = 1..20

(Measure-Command -Expression {
    $ArrayData = foreach ($i in 1..$ArrayDataRows) {
        [PSCustomObject] @{
            Number = $i
            Place = Get-Random -InputObject $PlaceList
            Color = Get-Random -InputObject $ColorList
            Zone = -join (Get-Random -InputObject $UC_LetterList -Count 10)
            Group = Get-Random -InputObject $GroupList
            }

        }
    }).TotalMilliseconds
# total ms = 24,390

讨论了关于构建数组(集合)的重要一般优化技术

解决这个难题的另一个重要部分是 如果可能,请避免在循环内调用(多个)cmdlet

使用 [random] (System.Random) 替换 Get-Random 调用提供了最大的加速(PSv5+ 语法):

$ArrayDataRows = 150000

$places = 'NJ', 'UT', 'NY', 'MI', 'PA', 'FL', 'AL', 'NM', 'CA', 'OK', 'TX', 'CO', 'AZ'
$colors = 'red', 'yellow', 'blue', 'purple', 'green', 'white', 'black'
$chars = [char[]] (65..90)
$nums = 1..20

# Instantiate a random number generator.
$rndGen = [random]::new()

$ArrayData = foreach ($i in 1..$ArrayDataRows) {
  [PSCustomObject] @{
     Number = $i
     Place = $places[$rndGen.Next(0, $places.Count)]
     Color = $colors[$rndGen.Next(0, $colors.Count)]
     Zone = -join $(
         $charList = [Collections.Generic.List[char]]::new($chars)
         foreach ($n in 1..10) { $randIndex = $rndGen.next(0, $charList.count); $charList[$randIndex]; $charList.RemoveAt($randIndex) }
       )
     Group = $nums[$rndGen.Next(0, $nums.Count)]
 }

在我的机器上,上面的命令大约需要 12 秒,而您的原始命令 运行 大约需要 35 分钟(!),相当于大约 175[=53 的加速因子=].


基准:

以下示例时间与您的原始方法、Lee 的优化版本以及上面基于 [random] 的解决方案进行了对比;绝对数字并不重要,但相对性能很重要,如 Factor 列所示:

1000个数组元素:

Factor Secs (10-run avg.) Command
------ ------------------ -------
1.00   0.100              # with [random]…
12.78  1.273              # with Get-Random - optimized…
13.45  1.340              # with Get-Random - original approach…

请注意,在 1000 个元素时,数组构建方法的优化提供了一些但不是很大的加速,但是元素越多,好处就越大。

10,000个数组元素:

Factor Secs (10-run avg.) Command
------ ------------------ -------
1.00   1.082              # with [random]…
12.29  13.296             # with Get-Random - optimized…
20.40  22.081             # with Get-Random - original approach…

对于 10,000 个元素,数组构建的优化已经得到了丰厚的回报。

我对 运行 150,000 元素没有耐心,但很容易改编下面的代码,它使用 Time-Command function:

$ArrayDataRows = 1000

$places = 'NJ', 'UT', 'NY', 'MI', 'PA', 'FL', 'AL', 'NM', 'CA', 'OK', 'TX', 'CO', 'AZ'
$colors = 'red', 'yellow', 'blue', 'purple', 'green', 'white', 'black'
$chars = [char[]] (65..90)
$nums = 1..20

Time-Command -Count 10 { # with [random]
    # Instantiate a random number generator.
    $rndGen = [random]::new()
    $ArrayData = foreach ($i in 1..$ArrayDataRows) {
      [PSCustomObject] @{
        Number = $i
        Place = $places[$rndGen.Next(0, $places.Count)]
        Color = $colors[$rndGen.Next(0, $colors.Count)]
        Zone = -join $(
            $charList = [Collections.Generic.List[char]]::new($chars)
            foreach ($n in 1..10) { $randIndex = $rndGen.next(0, $charList.count); $charList[$randIndex]; $charList.RemoveAt($randIndex) }
          )
        Group = $nums[$rndGen.Next(0, $nums.Count)]
      }
    }

  }, { # with Get-Random - optimized
    $ArrayData = foreach ($i in 1..$ArrayDataRows) {
       [PSCustomObject] @{
          Number = $i
          Place = Get-Random -InputObject $places
          Color = Get-Random -InputObject $colors
          Zone = -join (Get-Random -InputObject $chars -Count 10)
          Group = Get-Random -InputObject $nums
      }
    }
  } ,{ # with Get-Random - original approach
    $ArrayData = @()
    foreach ($i in 1..$ArrayDataRows) {
        $thisobject = [PSCustomObject] @{
            Number = $i
            Place = Get-Random -InputObject $places
            Color = Get-Random -InputObject $colors
            Zone = -join (Get-Random -InputObject $chars -Count 10)
            Group = Get-Random -InputObject $nums
        }
        $ArrayData += $thisobject 
    }
  }