【问题标题】:Runspace performance improvement less than expected运行空间性能提升低于预期
【发布时间】:2018-10-19 10:31:53
【问题描述】:

我正在分析一些代码以提高性能,但没有从运行空间中获得我期望的结果。

我的源文件是 7 个 Autodesk Revit 日志文件,从 13MB 和 150K 行到 90MB 和 900K 行不等。每个文件都包含多次引用它自己的名称,所以我将这个计数作为我以后想做的一些实际工作的代理。在下面的代码中,我使用简单的 foreach 处理所有文件,然后再次使用限制为 8 的运行空间。在这两种情况下,我都使用流阅读器来解析文件,因为文件可能会比我正在测试的文件大得多和。我不希望运行空间示例是循环时间的 25%,但我当然希望它更接近 25%,而不是 50%。相反,我看到的改进不到 50%。最后一次运行是单线程的 14.26 秒和 8 个运行空间的 8.74 秒。我在代码中做错了什么,还是我的期望不正确? FWIW 我目前正在虚拟机上进行测试。我尝试为 VM 分配 4、6、8 和 12 个内核,结果差别不大。最后一个测试是分配了 12 个内核,运行空间限制为 8 个。这是在主机上使用 10 个内核超线程 Xeon。

编辑:我修改了代码以将资源文件复制到 temp,以删除网络变量,并添加了一个基于作业的测试,再次限制为运行空间被限制到的相同 8 个并发线程。时间大致为 16.8 vs 9.6 vs 7.3。因此,Jobs 一直更好,尽管我的理解是运行空间更高效并且应该更快,而且性能仍然只比 50% 的节省好不了多少,即使有 8 个线程。

$source = '\\Px\Support\Profiling\Source'
$localSource = "$env:TEMP\Px"

Clear-Host

if (Test-Path $localSource) {
    Remove-Item "$localSource\*" -Recurse -force
} else {
    New-Item $localSource -ItemType:Directory > $null
}

Copy-Item "$source\*" $localSource
$journals = Get-ChildItem $localSource

Write-Host "Single Thread"
(Measure-Command {
    foreach ($journal in $journals) {
        $count = 0
        #$reader = [IO.StreamReader]::New($journal.fullName, $true)
        $reader = New-Object -typeName:System.IO.StreamReader -argumentList $journal.fullName
        while (-not ($reader.EndOfStream)) {
            $line = ($reader.ReadLine()).Trim()
            if ($line -match $journal) {
                $count ++
            }
        }
        Write-Host "$journal $count"
        $reader.Close()
        $reader.Dispose()
    }
}).totalSeconds
Write-Host

Write-Host "Runspace 1,8"
(Measure-Command {
    $runspacePool = [RunspaceFactory]::CreateRunspacePool(1,8)
    $runspacePool.Open()
    $runspaceCollection = New-Object system.collections.arraylist

    $scriptBlock = {
        param (
            [string]$journal
        )
        $journalName = Split-Path $journal -leaf
        $count = 0
        #$reader = [IO.StreamReader]::New($journal, $true)
        $reader = New-Object -typeName:System.IO.StreamReader -argumentList $journal
        while (-not ($reader.EndOfStream)) {
            $line = ($reader.ReadLine()).Trim()
            if ($line -match $journalName) {
                $count ++
            }
        }
        $reader.Close()
        $reader.Dispose()

    "$journalName $count"
    }

    foreach ($journal in $journals) {
        $parameters = @{
            journal = $journal.fullName
        }
        $powershell = [PowerShell]::Create()
        $powershell.RunspacePool = $RunspacePool
        $powershell.AddScript($scriptBlock) > $null
        $powershell.AddParameters($parameters) > $null
        $runspace = New-Object -TypeName PSObject -Property @{
            runspace   = $powershell.BeginInvoke() 
            powerShell = $powershell
        }
        $runspaceCollection.Add($runspace) > $null
    }
    while($runspaceCollection){
        foreach($runspace in $runspaceCollection.ToArray()){
            if($runspace.RunSpace.IsCompleted -eq $true){
                Write-Host "$($runspace.Powershell.EndInvoke($runspace.RunSpace))"
                $runspace.Powershell.dispose()
                $runspaceCollection.Remove($runspace)
                #[System.GC]::Collect()
                Start-Sleep -m:100
            }
        }
    }
}).totalSeconds
Write-Host

Write-Host "Jobs 8"
Remove-Job *
(Measure-Command {
    $scriptBlock = {
        param (
            [string]$journal
        )
        $journalName = Split-Path $journal -leaf
        $count = 0
        #$reader = [IO.StreamReader]::New($journal, $true)
        $reader = New-Object -typeName:System.IO.StreamReader -argumentList:$journal
        while (-not ($reader.EndOfStream)) {
            $line = ($reader.ReadLine()).Trim()
            if ($line -match $journalName) {
                $count ++
            }
        }
        $reader.Close()
        $reader.Dispose()

    Write-Output "$journalName $count"
    }
    foreach ($journal in $journals) {
        Start-Job -ScriptBlock:$scriptBlock -argumentlist:$journal.fullName
        While($(Get-Job -State 'Running').Count -ge 8) {
              sleep -m:100
         }
    }
    Get-Job | Wait-Job
    foreach ($job in  Get-Job) {
        Write-Host "$(Receive-Job $job)"
        Remove-Job $job
    }

}).totalSeconds
Write-Host

Remove-Item $localSource -Recurse -force

【问题讨论】:

  • 您是否忘记从 runespace 版本中排除 Start-Sleep -m:100 部分?
  • @kirill Start-Sleep 只是为了减少查询集合的开销。我试过没有,实际上更糟。非常有趣的是,我现在正在使用 Windows 10(VM 运行 Windows 7)的 Bootcamp 中对其进行测试,而在 Bootcamp 中几乎没有性能差异。我知道Win10很臃肿,但是老天,这对微软来说太尴尬了。
  • 问题 - 如果您只读取 7 个文件,为什么要使用 8 个线程?我看到其他人指出要使用的最佳最大线程数是 ProcessorCount+1。不要指望它本身会帮助解决问题,但可能有助于微调脚本。

标签: multithreading powershell runspace


【解决方案1】:

有趣的是,Start-Sleep 命令提高了您的性能 - 这表明您的 while($runspaceCollection) 循环是运行速度瓶颈的一部分。在你的脚本都设置好运行后,这个循环会不断地重新检查每个 RunSpace,并且只要一个完成就暂停 100 毫秒。我认为您以错误的方式构建了这一步 - 当它没有找到任何事情可做时,它可能更重要的是睡觉:

 while($runspaceCollection){
    foreach($runspace in $runspaceCollection.ToArray()){
        if($runspace.RunSpace.IsCompleted -eq $true){
            Write-Host "$($runspace.Powershell.EndInvoke($runspace.RunSpace))"
            $runspace.Powershell.dispose()
            $runspaceCollection.Remove($runspace)
        }else{
            Start-Sleep -m:100
        }
    }
}

或者,您可以使用if($RunspacePool.GetAvailableRunspaces() -eq 8) 之类的方式重构这部分代码,以便仅在运行空间全部完成后才开始清理它们。我已经在脚本结束时留下了超过一千个运行空间等待退役,而没有任何明显的性能下降,因此您可能会浪费系统资源,而不是提前清理。

除此之外,我建议您在运行脚本时监控本地计算机上的 CPU 和 RAM 使用情况,看看那里是否有任何明显的性能瓶颈表明它会变慢。

【讨论】:

    猜你喜欢
    • 2014-08-05
    • 2010-09-12
    • 2012-11-02
    • 2018-11-20
    • 1970-01-01
    • 2020-07-12
    • 1970-01-01
    • 2014-05-29
    • 1970-01-01
    相关资源
    最近更新 更多