问题
I'm working on a script for use in Windows 7 and Windows 10 for a Windows Registry search in HKLM:\Software\Classes. So far my code works, but it's extremely slow. It takes about 30 minutes to complete.
I need to use Set-Location also to avoid an error with Get-ItemProperty, which occurs because the $path is not a valid object.
How can I speed this code up? What's wrong?
File regsearch.ps1
(Mathias R. Jessen's answer applied)
Function Get-RegItems
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
#Set Local Path and ignore wildcard (literalpath)
Set-Location -literalpath $path
$d = Get-Item -literalpath $path
# If more than one value -> process
If ($d.Valuecount -gt 0) {
$d |
# Get unkown property
Select-Object -ExpandProperty Property |
ForEach {
$val = (Get-ItemProperty -Path . -Name $_).$_
#if Filter $match found, generate ReturnObject
if (($_ -match $match) -or ($val -match $match ) -or ($path-match $match)) {
New-Object psobject -Property @{ “key”=$path; “property”=$_; “value” = $val ;}
}
}
}
} #end function Get-RegItems
Function RegSearch
{
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match)
# Expand $path if necessary to get a valid object
if ($path.Indexof("HKEY") -ne "-1" -and $path.Indexof("Registry::") -eq "-1" ) {
$path = "Microsoft.PowerShell.Core\Registry::" +$path
}
# Retrieve items of the main key
Get-RegItems -path $path -match $match
# Retrieve items of all child keys
Get-ChildItem $path -Recurse -ErrorAction SilentlyContinue |
ForEach {
Get-RegItems -path $_.PsPath -match $match
}
} #end function RegSearch
#$search = "HKCU:\SOFTWARE\Microsoft\Office"
$searchkey = ‘HKLM:\SOFTWARE\Microsoft\Office\’
#$searchkey = "HKLM:\Software\Classes\"
$pattern = "EventSystem"
cls
$result = @()
Measure-Command {$result = Regsearch -path $searchkey -match $pattern }
# TESTING
#$t = @( "Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\Software\Classes",
# "HKLM:\Software\Classes\Wow6432Node\CLSID\",
# "HKCU:\SOFTWARE\Microsoft\Office\",
# "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office")
#cls
#$t |ForEach { Get-RegItems -path $_ } | fl
if ($result.Count) {
$result
"Count: {0}" -f ($result.Count-1)
}
else {
"Path: {0} `nNo Items found" -f $searchkey
}
回答1:
I accepted the challenge and made it "as fast as possible". Now it is even faster than REGEDIT or any other tool. The below sample lasts 11 seconds to parse the complete OFFICE-key and all subkeys.
In addition, it also searches for string-matches in REG-BINARY etc.
Enjoy!
# carsten.giese@googlemail.com
# reference: https://msdn.microsoft.com/de-de/vstudio/ms724875(v=vs.80)
cls
remove-variable * -ea 0
$ErrorActionPreference = "stop"
$signature = @'
[DllImport("advapi32.dll")]
public static extern Int32 RegOpenKeyEx(
UInt32 hkey,
StringBuilder lpSubKey,
int ulOptions,
int samDesired,
out IntPtr phkResult
);
[DllImport("advapi32.dll")]
public static extern Int32 RegQueryInfoKey(
IntPtr hKey,
StringBuilder lpClass, Int32 lpCls, Int32 spare,
out int subkeys, out int skLen, int mcLen, out int values,
out int vNLen, out int mvLen, int secDesc,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumValue(
IntPtr hKey,
int dwIndex,
IntPtr lpValueName,
ref IntPtr lpcchValueName,
IntPtr lpReserved,
out IntPtr lpType,
IntPtr lpData,
ref int lpcbData
);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
public static extern Int32 RegEnumKeyEx(
IntPtr hKey,
int dwIndex,
IntPtr lpName,
ref int lpcName,
IntPtr lpReserved,
IntPtr lpClass,
int lpcClass,
out System.Runtime.InteropServices.ComTypes.FILETIME lpftLastWriteTime
);
[DllImport("advapi32.dll")]
public static extern Int32 RegCloseKey(IntPtr hkey);
'@
$reg = add-type $signature -Name reg -Using System.Text -PassThru
$marshal = [System.Runtime.InteropServices.Marshal]
function search-RegistryTree($path) {
# open the key:
[IntPtr]$hkey = 0
$result = $reg::RegOpenKeyEx($global:hive, $path, 0, 25,[ref]$hkey)
if ($result -eq 0) {
# get details of the key:
$subKeyCount = 0
$maxSubKeyLen = 0
$valueCount = 0
$maxNameLen = 0
$maxValueLen = 0
$time = $global:time
$result = $reg::RegQueryInfoKey($hkey,$null,0,0,[ref]$subKeyCount,[ref]$maxSubKeyLen,0,[ref]$valueCount,[ref]$maxNameLen,[ref]$maxValueLen,0,[ref]$time)
if ($result -eq 0) {
$maxSubkeyLen += $maxSubkeyLen+1
$maxNameLen += $maxNameLen +1
$maxValueLen += $maxValueLen +1
}
# enumerate the values:
if ($valueCount -gt 0) {
$type = [IntPtr]0
$pName = $marshal::AllocHGlobal($maxNameLen)
$pValue = $marshal::AllocHGlobal($maxValueLen)
foreach ($index in 0..($valueCount-1)) {
$nameLen = $maxNameLen
$valueLen = $maxValueLen
$result = $reg::RegEnumValue($hkey, $index, $pName, [ref]$nameLen, 0, [ref]$type, $pValue, [ref]$valueLen)
if ($result -eq 0) {
$name = $marshal::PtrToStringUni($pName)
$value = switch ($type) {
1 {$marshal::PtrToStringUni($pValue)}
2 {$marshal::PtrToStringUni($pValue)}
3 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
if ($b[1] -eq 0 -and $b[-1] -eq 0 -and $b[0] -ne 0) {
[System.Text.Encoding]::Unicode.GetString($b)
} else {
[System.Text.Encoding]::UTF8.GetString($b)}
}
4 {$marshal::ReadInt32($pValue)}
7 {$b = [byte[]]::new($valueLen)
$marshal::Copy($pValue,$b,0,$valueLen)
$msz = [System.Text.Encoding]::Unicode.GetString($b)
$msz.TrimEnd(0).split(0)}
11 {$marshal::ReadInt64($pValue)}
}
if ($name -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
} elseif ($value -match $global:search) {
write-host "$path\$name : $value"
$global:hits++
}
}
}
$marshal::FreeHGlobal($pName)
$marshal::FreeHGlobal($pValue)
}
# enumerate the subkeys:
if ($subkeyCount -gt 0) {
$subKeyList = @()
$pName = $marshal::AllocHGlobal($maxSubkeyLen)
$subkeyList = foreach ($index in 0..($subkeyCount-1)) {
$nameLen = $maxSubkeyLen
$result = $reg::RegEnumKeyEx($hkey, $index, $pName, [ref]$nameLen,0,0,0, [ref]$time)
if ($result -eq 0) {
$marshal::PtrToStringUni($pName)
}
}
$marshal::FreeHGlobal($pName)
}
# close:
$result = $reg::RegCloseKey($hkey)
# get Tree-Size from each subkey:
$subKeyValueCount = 0
if ($subkeyCount -gt 0) {
foreach ($subkey in $subkeyList) {
$subKeyValueCount += search-RegistryTree "$path\$subkey"
}
}
return ($valueCount+$subKeyValueCount)
}
}
$timer = [System.Diagnostics.Stopwatch]::new()
$timer.Start()
# setting global variables:
$search = "enterprise"
$hive = [uint32]"0x80000002" #HKLM
$subkey = "SOFTWARE\Microsoft\Office"
$time = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
$hits = 0
write-host "We start searching for pattern '$search' in Registry-Path '$subkey' ...`n"
$count = search-RegistryTree $subkey
$timer.stop()
$sec = [int](100 * $timer.Elapsed.TotalSeconds)/100
write-host "`nWe checked $count reg-values in $sec seconds. Number of hits = $hits."
回答2:
The single biggest improvement you can make here is changing:
Set-Location -literalpath $path
$d= Get-Item .
to
$d = Get-Item -LiteralPath $path
Manipulating the location stack for each key in the hierarchy introduces A LOT of unnecessary overhead
回答3:
User function call overhead (scriptblocks included) is extremely big (e.g. 0.1-1ms). This becomes a very serious issue when the function is executed thousands/millions of times. Surprisingly, it's not mentioned in optimization-related articles (at least I've never seen it and I googled this topic a lot).
Unfortunately, the only only real solution to this particular issue is to inline the code at the cost of duplication and reduced readability.
Optimization should include code profiling.
PowerShell doesn't have a code profiler so you'll need to do it manually with Measure-Command.
Use System.Diagnostics.Stopwatch inside loops to display the accumulated time:
# global stopwatch
$sw1 = [Diagnostics.Stopwatch]::new()
$sw2 = [Diagnostics.Stopwatch]::new()
............
forEach(....) {
........
$sw1.start()
........
$sw1.stop()
........
$sw2.start()
........
$sw2.stop()
........
}
............
echo $sw1.ElapsedMilliseconds, $sw2.ElapsedMilliseconds
回答4:
Here is a faster version of you sample-script. Lasts ca. 1 minute on my machine. If you need it faster, then you need to work with advapi32.dll-Pinvokes, but then it will end quite complex.
Function Get-RegItems {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
#write-host $path.Substring(30)
$key = Get-Item -literalpath $path
ForEach ($entry in $key.Property) {
$value = $key.GetValue($entry)
if (($entry -match $match) -or ($value -match $match ) -or ($path -match $match)) {
write-host "key=$path property=$entry value=$value"
}
}
}
Function RegSearch {
Param(
[Parameter(Mandatory=$true)]
[string]$path,
[string]$match
)
Get-RegItems -path $path -match $match
ForEach ($item in get-ChildItem -literalpath $path -ea 0) {
RegSearch -path $item.PsPath -match $match
}
}
cls
Remove-Variable * -ea 0
[System.GC]::Collect()
$searchkey =‘HKLM:\SOFTWARE\Microsoft\Office’
$pattern = "EventSystem"
measure-command {
$result = RegSearch -path $searchkey -match $pattern
}
回答5:
Don't use the registry drive provider, if you want it faster.
I've also read classes with static methods are faster.
来源:https://stackoverflow.com/questions/39221709/speed-up-powershell-script-for-windows-registry-search-currently-30-minutes