PowerShell性能优化系列文章
概述
PowerShell已经存在很长一段时间了,许多基本的问题都得到了很好的解决。对于脚本编写者来说,借助于PowerShell的Get-Help帮助文档,互联网,和社区资源,绝大多数普通的任务应当用什么,怎么样来完成,都应该能快速找到答案。一旦脚本写好了,没有bug,准备投入到生产环境中了,可能另外一些问题会浮出水面,正好也是我们平时讨论的比较少的。怎么尽可能的写出更好的脚本?将已经正在使用的脚本改进的更加高效,强化工具。而这些正好需要一些不同的问题和思想来处理。这样就会开始把脚本编写者从最终用户和管理员的世界中解放出来,进入开发者的领域。虽然有些人很不情愿捡起另外一顶帽子,混入开发圈。但是真的没有比PowerShell更好的工具为你在平行宇宙中打开另一扇门。因此,让我们来看一眼一些最大化提高脚本性能的基本概念,然后转到一些深刻的问题上,看看我们离挑战极限还有多大的差距。
PowerShell 有两个关键领域没有引起它们应得的关注,那就是优化和性能测试。所谓优化,就意味着脚本的设计和修改要尽可能减少在运行过程中时间和资源的占用。简而言之,就是要改造代码,让它快速和高效地运行。性能测试总是与优化并驾齐驱,通过脚本执行的结果,来决定优化是否有效。这种方式真的可以搞清楚所谓的“改造”,是不是真的改好了。同时还有一种优化无需性能测试即可避免很多问题。这里引用一下,我曾多次看到关于无测试优化的一句话:
优秀的工程源于可预知的成本创造可预知的结果。我想说的是没有测量就没有工程。
“没有测量就没有工程” 确实, 这是一篇旧文章,但是它的观点放在现在依然像它刚发表时一样有意义。唯一的途径就是真诚地验证你所有的返工是否能从现实世界的结果中得到好处。而,性能测试是完成它的关键方式。
最佳执行的默认顺序
根据刚才拟定的条款,让我们讨论一些应当提前了解的PowerShell的基础知识点。更好地认识PowerShell是前提。如果连基本的知识都不了解,探讨高级的概念价值也不大。比如,Bruce Payette列出最佳执行顺序的提纲,它是基于你正在运行的是那一类命令。在2.1.2章节(命令分类中),从30页到34页,他说按照执行速度的快慢,命令的类型顺序如下:
Cmdlets:编译过的二进制文件
函数:驻留在内存中的脚本代码
脚本(脚本命令):保存在ps1 文件中的函数或命令
原生的Win32可执行文件:传统的可执行文件
因此,如果你有一个Win32可执行文件,可能把它移植进一个脚本,一个函数,甚至一个cmdlet可能会带来速度的提升。这可能并不是永远都可行,但是,有很多场景把你的功能移植到上游可以对你的PowerShell代码的执行效率带来改善。这里的关键点是理解怎样使用不同种类的命令可能会影响你的脚本最终优化的程度。
优化注意事项
通过执行优化的基本顺序,我们可以通过提出一些问题,然后根据这些问题来开始完善你的脚本。
执行优化时,下面的这些问题会被经常问到:
这种情况下我是否使用了正确的构造函数?
我是否做了很多不需要的操作?
我是否使用了太多的对象?
我是否使用了很多共享资源?
我使用的集合合适吗?换个问法:我是否使用了正确或者最好的集合来完成这个任务?
管道是最好的途径吗?
是否有另外的命令模型可能会更好?
我对于对象的思考是否合适?
我有没有使用-filter(如果有)?
我预定义的变量是不是没有用到?
对于一些任务,使用Job和Runspaces是不是有用?
我的循环是否设计的高效?
我使用的ForEach-Object,如若使用For会不会更好?
当你的脚本中有 ForEach-Object时,你有没有用到begin-process-end结构?
你有没有使用高级的函数?
你有没有做参数检查?
你有没有使用严格模式(Strict-Mode)?
当我远程连接时,比如活动目录,我又没有限制返回对象的数量?
当我从网路中获取数据时,我又没有使用filter,where和其它参数指定返回值,以限制返回的结果的数量?
我有没有第一次拿到数据时,把它保存到变量中,以便于下次用到?
是不是有一个低层次的方案更高效?比如.NET?P/Invoke?
你是不是使用管道传递集合而不是使用一个大集合存储?
你是不是测试了一个没必要的条件判断?
下面来演示为什么上述提问可能会造成问题:
我使用了-filter吗(如果有)?
下面的脚本片段演示怎样使用适当的命令的-Filter参数来较强地提升效率。例如我使用Get-ChildItem -Filter和Get-ChildItem | Where {$_.Name -eq ‘100Mb.txt’搜索一个文件“C:\test\100Mb.txt”。
Clear-Host
1..10 |
ForEach-Object {
"Command: output $_"
"-" * 25
$test1 = { Get-ChildItem -Path C:\test -Filter 100mb.txt | Out-Null }
$test2 = { Get-ChildItem -Path C:\test | Where {$_.Name -eq '100Mb.txt'} | Out-Null }
$results1 = (Measure-Command -Expression $test1).Ticks
$results2 =(Measure-Command -Expression $test2).Ticks
"{0}`t`t{1}" -f '-filter',$results1
"{0}`t`t`t{1}" -f 'Where',$results2
""
"{0}`t`t{1:N}" -f 'Difference',($results1/$results2)
""
}
测试证明,在这个很小的案例中,使用-Filter参数比备受青睐的ForEach-Object中使用Where,速度可以提升4-8倍。我们通常情况下,喜欢用foreach,但是简单的过滤器过滤,可以非常有意义的提升脚本速度。
我使用的ForEach-Object,如若使用For会不会更好?
下面的脚本片段我们使用ForEach-Object来替换for循环进行测试:
"Command: output 1"
"-" * 25
$test1 = { 1..1000 | % { 1 } }
$test2 = { for($i = 1; $i -le 1000; $i++) { 1 }}
$results1 = (Measure-Command -Expression $test1).Ticks
$results2 =(Measure-Command -Expression $test2).Ticks
"{0}`t`t{1}" -f 'foreach',$results1
"{0}`t`t`t{1}" -f 'for',$results2
""
"{0}`t`t{1:N}" -f 'Difference',($results1/$results2)
""
"Command: evaluate 1 -eq 1"
"-" * 25
$test3 = { 1..1000 | % { 1 -eq 1 } }
$test4 = { for($i = 1; $i -le 1000; $i++) { 1 -eq 1 }}
$results3 = (Measure-Command -Expression $test3).Ticks
$results4 =(Measure-Command -Expression $test4).Ticks
"{0}`t`t{1}" -f 'foreach',$results3
"{0}`t`t`t{1}" -f 'for',$results4
""
"{0}`t`t{1:N}" -f 'Difference',($results3/$results4)
这段代码在我的机器上执行后,结果如下:
Command: output 1
-------------------------
foreach 903091
for 18212
Difference 49.59
Command: evaluate 1 -eq 1
-------------------------
foreach 907313
for 22254
Difference 40.77
测试表明ForEach-Object在我的机器上比直接使用For循环要慢40-50倍。因为ForEach对集合进行迭代和控制,是需要时间的。所以这也是一点,全面优化脚本时需要考虑的。
我是不是测试了一个没必要的条件判断?
很多时候,我们用来判断条件是真或者假时,有一些更简单的实际上无须评估的方式。比如像这样判断一个变量是不是true?
if(1 -eq $true)
{
Do-Something
}
你可以非常简单地把判断直接交给If就够了。
if(1)
{
Do-Something
}
为了测试上面的区别,我简单写了一段脚本,发现跳过If中的判断,可以换取大概5%的性能。
$test_001 = { if(1 -eq $true) {1} else {0}}
$test_002 = { if(1) {1} else {0}}
Get-ChildItem variable:\test_* |
ForEach-Object {
Measure-Command -Expression {1..100000 | % { $_ }} |
select TotalSeconds
}
运行后的结果:
TotalSeconds
------------
12.1220562
11.732413
例子虽小,但是可以说明如果一个布尔类型的判断在IF语句中,是可以直接省略判断,反而能提升性能。
性能测试注意事项
性能测试有很多需要注意的地方,下面会列出来一些关键点,用来审查你的命令和脚本。
你是否测试过足够多的迭代次数和变化,来取平均结果?
你的测试会随着数据,结构系统波动吗?
测试的结果能否重现?
你能解释为什么你的测试能验证你的改动和优化?
如果第一次迭代比后续的迭代速度要快,可能是结果被缓存了。果真这样,可以在迭代之间插入一个停顿,刷新缓存,然后得到一致的结果。
其它资源
×用微信扫描并分享