填充非常大的哈希表 - 如何最有效地做到这一点?

Populating very large hashtables - how to do so most efficiently?

Background/Context:

我必须交叉检查/比较多个数据集(它们往往相互不一致),以便识别 "item X from dataset A matches item Y or Z from dataset B"。

涉及的那些数据集有些大(100k 条记录)并且需要我戳一个 SQL 数据库。

经过一些初步的研究和性能测试,我已经从通过 "massive arrays" 解析转换为有效地使用 "Indexing hashtables" 关键属性点。

挑战:

使用 Hashtables 非常 一旦你开始使用它们......但我的问题是有效地构建它们。感觉我是 "nearly there",但不得不求助于(相对)慢的方法(50,000 条记录大约需要 300-400 秒)。

这是我现在尝试索引的基本数据的样子(我从 SQL 中获得了不同设备名称的列表以及它有多少条记录的计数设备):

DEVICENAME      COUNTOF
==========      ========
DEVICE_1        1
DEVICE_2        1
DEVICE_3        2
....            ...
DEVICE_49999    3
DEVICE_50000    1

当前解法:

我目前正在通过遍历结果集(我从 SQL 中提取的数组作为结果集)并为每个使用“.add”来构建我的散列table订单项。

所以只是一个简单的...

for ($i=0; $i -lt @($SQL_Results).CountOf; $i++) {
    $MyIndexHash.Add( @($SQL_Results[$i]).DeviceName,  @($SQL_Results[$i]).CountOf)
}

这是"a bit slow"相对而言的(上述300-400秒构建50,000个订单项)。如果需要,我 可以 等待,但是因为(预感)我尝试了以下 "near instant",它取笑说可能有更好的方法来做到这一点(采取了大约 3 秒)。

$MyIndexHash.Keys = $SQL_Results.DEVICENAME

但是 - 此 ONLY 填充了散列的 KEYStable,而不是相关值。而且我还没有想出一种有效实现以下目标的方法(将数组中的值直接分配到散列中table):

$MyIndexHash.Keys = ($SQL_Results.DEVICENAME, $SQL_Results.COUNTOF)

这是一个 "pure performance" 问题 -- 因为我需要做的一些其他比较将涉及 80,000 和 150,000 个行项目。如果我必须 "just wait" 通过遍历 SQL 结果数组的每一行来构造散列 table,那就这样吧。

注意 - 我看过 - Powershell 2 and .NET: Optimize for extremely large hash tables? - 但是因为我有可变的(很好 - "unknown but likely large")数据集来处理我不确定我可以/想要开始分解哈希table。

此外,散列tables 中的查找(一旦填充)毕竟超快......我希望它只是散列tables 的构造以某种方式以更有效的方式完成?

欢迎就如何更有效地构建哈希 [​​=108=] 提出任何建议。

谢谢!

更新/调查

根据 @Pawel_Dyl 对 hash-table 分配应该多快的评论让我对我的代码变体和更大的(200k 行项目)数据集进行了一些调查值。

以下是测试结果以及持续时间:

#Create the Demo Data... 200k lines
$Src = 1..200000 | % { [pscustomobject]@{Name="Item_$_"; CountOf=$_} }

# Test # 1 - Checking (... -lt $Src.Count) option vs (... -lt @($Src)Count ) ...
# Test 1A - using $Src.CountOf
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash1A = @{}
foreach ($i in $Src) { $hash1A[$i.Name] = $i.CountOf }
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration = 736 ms

# Now with @()
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash1B = @{}
foreach ($i in @($Src)) { $hash1B[$i.Name] = $i.CountOf }
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration = 728 ms

##################

# Test # 2 - Checking (... -lt $Src.Count) option vs (... -lt @($Src).Count ) ...

$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash2A = @{}
for ($i=0; $i -lt @($Src).Count; $i++) {
    $hash2A.Add(@($Src[$i]).Name, @($Src[$i]).CountOf)
}
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration == 4,625,755 (!) (commas added for easier readability!

$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash2B = @{}
for ($i=0; $i -lt $Src.Count; $i++) {
    $hash2B.Add( $Src[$i].Name, $Src[$i].CountOf )
}
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration == 1788 ms

所以问题来自使用@()-s 在循环中引用数组。意味着作为对 SQL 的单行数组/结果的保护(由于某些奇怪的原因,Powershell 没有作为一个概念,而是完全不同地对待 DATAOBJECT 而不是数组(所以像 .Count 这样的东西如果不强制 POSH 通过 @() 将其作为数组处理,则不可用。

所以解决方案 "for now" 是添加一个简单的... If (@($MyArray).Count -eq 1) {用@() 做事} ElseIf (@($MyArray).Count -gt 1) {不使用@()-s 做事}

这就是我们的罪魁祸首 - 在循环中使用 @()-s 花费了将近 1.25 小时,而同样的操作花费了约 1 秒。

改变已经大大加快了事情的速度(只需要 0.1 秒来构建每个散列table,即使处理了 90,000 多个对象"in anger"。代码稍微不太方便,但是哦,好吧。我仍然不明白为什么 Powershell 对“单行数组”的概念有疑问,并决定以不同的方式/作为单独的数据类型来处理这些问题,但是你去吧。

我仍会查看 DataReader 的建议,以了解我在何处/如何在代码中最好地利用它们作为未来的改进。非常感谢所有的建议和很好的解释,让一切都变得有意义!

我预计性能瓶颈在哈希表之外。我测量了我看到的最常见的方法,结果如下:

#demo data
#$src = 1..200000 | % { [pscustomobject]@{Name="Item_$_";Count=$_} }

#1
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash1 = @{}
$src | % {$hash1[$_.Name]=$_.Count}
$timer.Stop()
$timer.ElapsedMilliseconds

#2
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash2 = @{}
for ($i=0; $i -lt $src.Count; $i++) {
    $hash2.Add($src[$i].Name,$src[$i].Count)
}
$timer.Stop()
$timer.ElapsedMilliseconds

#3
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash3 = @{}
foreach ($i in $src.GetEnumerator()) { $hash3[$i.Name] = $i.Count }
$timer.Stop()
$timer.ElapsedMilliseconds

#4
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash3 = @{}
foreach ($i in $src) { $hash3[$i.Name] = $i.Count }
$timer.Stop()
$timer.ElapsedMilliseconds

在我的电脑上分别用了 ~5s、~1.7s、~0.7s、~0.7s 完成第 1-4 部分(200000 条记录)。如果必须进一步优化,我会测量一些用于构建字典的本机 .NET 方法。

尝试优化其余代码。提示:

  • 你确定所有记录在你的hastable循环之前都在内存中吗 开始?
  • 您确定属性是简单类型吗(int、string - 小心 代理,带有 "hidden" 代码的属性)?

注意:我强烈,强烈建议您不要使用Count作为输出列的名称,因为那与 PowerShell 中的默认 属性 冲突。示例:@().Count returns 0。您的代码可能有效,但它非常模棱两可。强烈建议将您的查询更改为使用 DeviceCount 或类似的查询。


在 PowerShell 中获得此信息的绝对最快方法是使用 SqlDataReader 完成所有操作并直接循环输出。假设你的数据源是 SQL Server:

$ConnectionString = 'Data Source={0};Initial Catalog={1};Integrated Security=True' -f $SqlServer, $Database
$SqlConnection = [System.Data.SqlClient.SqlConnection]::new($ConnectionString)
$SqlCommand = [System.Data.SqlClient.SqlCommand]::new($SqlQuery, $SqlConnection)

$Data = @{}
$SqlConnection.Open()
try {
    $DataReader = $SqlCommand.ExecuteReader()
    while ($DataReader.Read()) {
        $Data[$DataReader.GetString(0)] = $DataReader.GetInt32(1)
    }
}
finally {
    $SqlConnection.Close()
    $SqlConnection.Dispose()
}

在我的系统上,我可以在大约 700 毫秒内获取和处理 160,000 条记录(请记住,我没有使用聚合函数)。

使用 $Data.Add($DataReader.GetString(0), $DataReader.GetInt32(1)) 语法而不是 $Data[$DataReader.GetString(0)] = $DataReader.GetInt32(1) 对我来说大约慢 20%。但是,此方法确实有一个重要的警告。 $HashTable.Add($Key, $Value) 将在重复键上引发错误。 $HashTable[$Key] = $Value 只会默默地替换该值。确保您的 SQL 查询正确并且不会 return 重复值

您也可以使用 $DataReader['DeviceName'] 而不是 $DataReader.GetString(0),但这意味着 SqlDataReader 必须进行查找,因此速度稍慢(大约 10%)。使用 GetX() 方法的缺点是 a) 参数 01 指的是列顺序,因此您必须知道输出的列顺序(通常没什么大不了的) b) 你必须知道输出的数据类型(通常也没什么大不了的)。

我没有发现在第一个 运行 上使用字典而不是哈希表有显着的性能差异,但在第一个 运行 之后,使用字典的速度提高了大约 20%。就是说,运行冷的我看不出有什么区别。 运行热我看词典运行ning快了。你不妨测试一下。如果是这样,请不要使用 $Data = @{},而是使用:

$InitialSize = 51000 # The more accurate this guess is without going under, the better
$Data = [System.Collections.Generic.Dictionary[String,Int32]]::new($InitialSize)

为了进一步参考,如果您需要对 SQL 结果集进行更快的查找,而您的查找 do 具有重复的查找值,则 usually fastest to use a DataView,排序时确实使用索引进行搜索:

$ConnectionString = 'Data Source={0};Initial Catalog={1};Integrated Security=True' -f $SqlServer, $Database
$SqlConnection = [System.Data.SqlClient.SqlConnection]::new($ConnectionString)
$SqlCommand = [System.Data.SqlClient.SqlCommand]::new($SqlQuery, $SqlConnection)

$DataTable = [System.Data.DataTable]::new()
$SqlConnection.Open()
try {
    $DataReader = $SqlCommand.ExecuteReader()
    $DataTable.Load($DataReader)
}
finally {
    $SqlConnection.Close()
    $SqlConnection.Dispose()
}

$DataView = [System.Data.DataView]::new($DataTable)
$DataView.Sort = 'DeviceName' # Create an index used for Find() and FindRows()
$DataView.Find('DEVICE_1') # -1 means not found, otherwise it's the index of the row
$DataView.FindRows('DEVICE_1')

您可以使用 DataAdapter 或 DataSet;我刚刚选择在这里只使用一个数据表,因为我有代码已经这样做了。