此 Powershell 代码片段的哪一部分导致 运行 花费很长时间?

Which part of this Powershell code snippet is making it take a long time to run?

我的任务是为我们的 AD 环境中的每个用户制作上次登录时间的报告,显然我首先向母亲 google 询问了一些我可以重新利用但找不到任何可以改变用途的东西检查多个域控制器并协调最后一个,然后吐出它是否超过了任意设置的 date/number 天。

代码如下:

foreach ($user in $usernames) {
    $percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)
    Write-Progress -Id 3 -Activity "Finding Inactive Accounts" -Status "$($percentCmpUser)% Complete:" -PercentComplete $percentCmpUser
    $allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}
    $finalLogon = $allLogons| Sort-Object LastLogon -Descending |Select-Object -First 1
    if ($finalLogon.LastLogon -lt $time.ToFileTime()) {
        $inactiveAccounts += $finalLogon
    } 
}

$usernames 是大约 6000 个用户名的列表

$AllUsers 是一个包含 18000 个用户的列表,其中包含我希望在最终报告中访问的 10 个不同属性。我得到它的方法是为我关心的特定 OU 中的所有用户点击我们 20 个左右 DC 中的三个。最终脚本实际上是 6k*20,因为我确实需要访问每个 DC 以确保我不会错过任何用户的登录。

$time 的计算方法如下:

$DaysInactive = 60
$todayDate = Get-Date
$time = ($todayDate).Adddays(-($DaysInactive))

每个变量都在脚本的其他地方使用,这就是为什么我将其拆分成这样。

在你建议 LastLogonTimestamp 之前,有人告诉我它不够新,当我询问是否要将复制时间更改为更新时,我被告知 "no, not gonna happen"。

Search-ADAccount 似乎也没有提供非活跃用户的准确视图。

我乐于接受有关如何使这个特定代码段 运行 更快或如何使用不同的方法在短时间内获得相同结果的所有建议。

截至目前,为特定 OU 中的所有用户访问每个 DC 每个 DC 大约需要 10-20 秒,然后上面的代码片段需要 30-40 分钟。

有几件事很突出,但这里最大的性能杀手可能是以下两个语句:

$percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)
# and
$allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}

...这两个语句都将表现出 O(N^2)(或 quadratic)性能特征 - 也就是说,每次你 双倍 输入大小,花费的时间 四倍!


  1. Array.IndexOf() 实际上是一个循环

我们来看第一个:

$percentCmpUser = [math]::Truncate(($usernames.IndexOf($user)/$usernames.count)*100)

这可能不是不言而喻的,但是这个方法调用:$usernames.IndexOf() 可能需要遍历整个每次执行时 $usernames 的列表 - 当您到达最后一个 $user 时,它需要遍历并比较 $user 所有 6000 个项目。

解决此问题的两种方法:

使用常规 for 循环:

for($i = 0; $i -lt $usernames.Count; $i++) {
    $user = $usernames[$i]
    $percent = ($i / $usernames.Count) * 100
    # ...
}

完全停止输出进度

Write-Progress 真的很慢 - 即使调用者抑制 Progress 输出(例如 $ProgressPreference = 'SilentlyContinue'),使用进度流仍然会带来开销,尤其是在每次循环迭代中调用时。

完全删除 Write-Progress 将删除计算百分比的要求:)

如果您仍然需要输出进度信息,您可以通过仅调用 Write-Progress 有时 来减少一些开销 - 例如每 100 次迭代一次:

for($i = 0; $i -lt $usernames.Count; $i++) {
    $user = $usernames[$i]
    if($i % 100 -eq 0){
        $percent = ($i / $usernames.Count) * 100
        Write-Progress -Id 3 -Activity "Finding Inactive Accounts" -PercentComplete $percent
    }
    # ...
}

  1. ... |Where-Object 也是只是一个循环

现在是第二个:

$allLogons = $AllUsers | Where-Object {$_.SamAccountName -match $user}

... 6000 次,powershell 必须枚举 $AllUsers 中的所有 18000 个对象并针对 Where-Object 过滤器测试它们。

考虑将所有用户加载到哈希表中,而不是使用数组和 Where-Object

# Only need to run this once, before the loop
$AllLogonsTable = @{}
$AllUsers |ForEach-Object {
    # Check if the hashtable already contains an item associated with the user name
    if(-not $AllLogonsTable.ContainsKey($_.SamAccountName)){
        # Before adding the first item, create an array we can add subsequent items to
        $AllLogonsTable[$_.SamAccountName] = @()
    }

    # Add the item to the array associated with the username
    $AllUsersTable[$_.SamAccountName] += $_
}

foreach($user in $users){
    # This will be _much faster_ than $AllUsers |Where-Object ...
    $allLogons = $AllLogonsTable[$user]
}

哈希表具有 疯狂快速 查找 - 通过键 查找对象要快得多在数组上使用 Where-Object