Board logo

标题: [系统相关] 讨论:批处理中for /f 解析命令输出的效率 [打印本页]

作者: 随风    时间: 2009-3-5 09:31     标题: 讨论:批处理中for /f 解析命令输出的效率

讨论:for + 命令 的效率
by 随风 @bbs.bathome.net @2009-03-03
前不久的一个关于数字排序的帖子中曾提到过一个没太引人注意的问题。
1、for /f 的集中 即:in 后面的括弧中 使用命令会不会影响代码的运行效率。
   如果会,到底有多大的影响,依据是什么。
2、findstr 命令,真的很影响效率吗,它在速度方面的优、缺点都是怎样的。
正好这几天有空,专门对这几个问题作了些测试,拿出来讨论一下。
在batman的数字排序帖中,我曾说for的in 中使用任何命令都会影响效率,现证实如下:
测试环境:xp-sp2 cup 1.00GHz  内存:128MB
测试环境极为垃圾,各位若要测试,可能需要将样本文件体积放大,才能看出时间差别。
测试最终得到的结果不一定精确,但应该足以说明一些问题。
以下所有测试都需 time0.bat 的配合
.
time0.bat 内容
  1. :time0  计算时间差 (封装)
  2. @echo off&setlocal&set /a n=0&rem code 随风 @bbs.bathome.net
  3. for /f "tokens=1-8 delims=.: " %%a in ("%~1:%~2") do (
  4. set /a n+=10%%a%%100*360000+10%%b%%100*6000+10%%c%%100*100+10%%d%%100
  5. set /a n-=10%%e%%100*360000+10%%f%%100*6000+10%%g%%100*100+10%%h%%100)
  6. set /a s=n/360000,n=n%%360000,f=n/6000,n=n%%6000,m=n/100,n=n%%100
  7. set "ok=%s% 小时 %f% 分钟 %m% 秒 %n% 毫秒"
  8. endlocal&set %~3=%ok:-=%&goto :EOF
复制代码

测试一、
先用一个公认不耗时的内部命令(set 命令)来测试。
代码流程:
   先设置10个以点为开头的变量,再运行10次 for /f + set . 来显示,总共显示 100个结果。
   与 for 直接显示 1000 个结果来对比。
  1. @echo off&setlocal enabledelayedexpansion
  2. set "str="
  3. for /f "delims=" %%a in ('set . 2^>nul') do set "%%a="
  4. for /l %%a in (1 1 10) do set .%%a=%%a&set str=!str! %%a
  5. set t=%time%
  6. for /l %%j in (1 1 100) do (
  7.   for /f "delims=" %%a in ("%str%") do (
  8.     for %%i in (%%a) do echo %%i >nul
  9. ))
  10. call time0 %t% %time% ok
  11. echo  for /f 命令 直接显示1000个结果 %ok%
  12. echo.
  13. for /l %%a in (1 1 10) do call :loop %%a
  14. pause
  15. :loop
  16. set t=%time%
  17. for /f "delims=" %%a in ('set .') do echo %%a >nul
  18. call time0 %t% %time% ok
  19. echo 运行%1次 %ok%
  20. goto :EOF
复制代码

测试一 结果:
for /f 命令 直接显示1000个结果 0 小时 0 分钟 0 秒 12 毫秒
运行1次 0 小时 0 分钟 0 秒 6 毫秒
运行2次 0 小时 0 分钟 0 秒 7 毫秒
运行3次 0 小时 0 分钟 0 秒 6 毫秒
运行4次 0 小时 0 分钟 0 秒 7 毫秒
运行5次 0 小时 0 分钟 0 秒 6 毫秒
运行6次 0 小时 0 分钟 0 秒 7 毫秒
运行7次 0 小时 0 分钟 0 秒 6 毫秒
运行8次 0 小时 0 分钟 0 秒 7 毫秒
运行9次 0 小时 0 分钟 0 秒 7 毫秒
运行10次 0 小时 0 分钟 0 秒 6 毫秒
.
可以看出 直接用 for 显示 1000 个结果耗时只需 12 毫秒
而用 for /f + set . 总共显示100个结果平均每次耗时最少6毫秒,
如果显示1000个结果那么效率可想而知。
难道启动 set 命令这么耗时吗?很简单作一次测试便知:
  1. @echo off&setlocal enabledelayedexpansion
  2. for /l %%a in (1 1 10) do set .%%a=%%a
  3. set t=%time%
  4. for /l %%a in (1 1 100) do set . >nul&echo a>nul
  5. call time0 %t% %time% ok
  6. echo  %ok%
  7. pause
复制代码

耗时:0 小时 0 分钟 0 秒 3 毫秒
结果显示启动100次耗时才 3 毫秒,那为何上面的耗时这么多呢?继续看下面的测试。
======================================================================
记得我曾经发过一个关于代码运行效率的帖,里面说批处理中频繁使用findstr命令
会大大影响代码效率,现测试该说法并不完全正确。
测试二、
   测试每启动一次 findstr 命令需要多少时间。
   测试样本文件 a.txt 和 c.txt
   其中:
   a.txt 有5000行 大小:290KB
   c.txt 一行 大小:1KB
  1. @echo off
  2. echo a>c.txt
  3. set t=%time%
  4. findstr .* c.txt>nul
  5. call time0 %t% %time% ok
  6. echo %Ok%
  7. set t=%time%
  8. findstr .* a.txt>nul
  9. call time0 %t% %time% ok
  10. echo %Ok%
  11. set t=%time%
  12. for /l %%a in (1 1 100) do findstr .* c.txt>nul
  13. call time0 %t% %time% ok
  14. echo %Ok%
  15. set t=%time%
  16. for /l %%a in (1 1 100) do findstr .* a.txt>nul
  17. call time0 %t% %time% ok
  18. echo %Ok%
  19. pause
复制代码

测试二 结果
启动一次,遍历 1kb 的文件 0 小时 0 分钟 0 秒 7 毫秒
启动一次,遍历 290kb 的文件 0 小时 0 分钟 0 秒 8 毫秒
启动100次,遍历 1kb 的文件 0 小时 0 分钟 7 秒 33 毫秒
启动100次,遍历 290kb 的文件 0 小时 0 分钟 7 秒 42 毫秒
.
可以看出,findstr 命令无论是遍历大文件,还是小文件,耗时是差不多的
即使是启动100次,平均耗时也与启动一次差不多,
因此得出findstr的启动耗时应该是 7 毫秒左右。(经测试:find命令也是一样)
得此结果,便可根据实际情况来决定是否该在for中来使用findstr命令了。
也可证明findstr 遍历文件的速度是很快的,且不受文件大小影响。
事实上 findstr 写入文件的速度也是快的惊人,最后有作测试。
======================================================================
为何 for /f + 命令 有时会使效率大大降低。
测试三、
  1. @echo off&setlocal enabledelayedexpansion
  2. cd.>c.txt
  3. for /l %%a in (1 1 5000) do (
  4.    echo bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb>>c.txt
  5. )
  6. call :jjj 1
  7. call :loop 1 a&echo.
  8. call :jjj 1
  9. call :loop 2 a&echo.
  10. call :jjj 98
  11. call :loop 100 a&echo.
  12. call :loop 5000 b&echo.
  13. pause
  14. :loop
  15. set t=%time%
  16. for /f "delims=" %%a in ('findstr "%2" c.txt') do set a=a
  17. call time0 %t% %time% ok
  18. echo 匹配结果为%1行 %ok%
  19. goto :EOF
  20. :jjj
  21. for /l %%a in (1 1 %~1) do echo a>>c.txt
  22. goto :EOF
复制代码

测试三 结果:
findstr匹配结果为1行时耗时: 0 小时 0 分钟 0 秒 15 毫秒
findstr匹配结果为2行时耗时: 0 小时 0 分钟 0 秒 14 毫秒
findstr匹配结果为100行时耗时: 0 小时 0 分钟 0 秒 14 毫秒
findstr匹配结果为5000行时耗时:0 小时 0 分钟 2 秒 19 毫秒
.
可以看出 for /f + findstr 命令
当 findstr 匹配的结果为 1-100 行时,耗时是差不多的。
但当匹配结果有 5000 行时,效率明显降低。
再结合第二个测试结果,每启动一次 findstr 命令都耗时 7 毫秒。
再用这里的 15-7=8 也就是说多用了 8 毫秒
那这 8 毫秒是哪里耗去了呢?
我认为:
这是管道消耗的时间,for /f + 命令  命令所得到的结果是通过管道传递给for的,
而管道的耗时 大约是在 6-8 毫秒之间,这就能解释为什么 for + set 要耗时 6 毫秒
for + findstr 要耗时 15 毫秒(直接启动findstr命令耗时 7 毫秒)
但为什么匹配结果为5000行时,同样是启动一次findstr 而时间却要如此之多呢?
还是管道,管道就向一辆汽车,最多载100人,如果超出100,哪怕只超出1人,
那么这辆汽车也要多开一次,耗时也就多一次。但即使是只有1人,它也必须开一次。
测试管道命令耗时代码
  1. @echo off
  2. set t=%time%
  3. echo a|findstr .*>nul
  4. call time0 %t% %time% ok
  5. echo  %ok%
  6. pause
复制代码

耗时:0 小时 0 分钟 0 秒 15 毫秒
正好等于 for + findstr 的耗时结果
最后总结:
“for + 命令” 运行效率等于 for耗时+命令耗时+管道耗时(数据多时,管道耗时同时增加)
效率总结:
代码中过多使用 for + 命令 会在一定程度上影响效率,每次最少消耗 6 毫秒左右。
findstr 命令每启动一次至少消耗 7 毫秒左右。
另外值得一提的是:findstr 命令有众多开关,
当使用某些开关时,findstr的运行效率会受文件大小的影响,比如: /in /ix
.
最后再作几个写入文件速度的测试
1、for /f 直接循环
2、for /f + 改变句柄指向
3、findstr写入文件
样本文件c.txt 为5000行,大小:281KB
  1. @echo off&setlocal enabledelayedexpansion
  2. set t=%time%
  3. cd.>d.txt
  4. for /f "delims=" %%a in (c.txt) do >>d.txt echo %%a
  5. call time0 %t% %time% ok
  6. echo  %ok%
  7. set t=%time%
  8. echo.>nul 3>d.txt
  9. for /f "delims=" %%a in (c.txt) do echo %%a
  10. echo.>nul 4>con
  11. call time0 %t% %time% ok
  12. echo  %ok%
  13. set t=%time%
  14. findstr .* c.txt>e.txt
  15. call time0 %t% %time% ok
  16. echo  %ok%
  17. pause
复制代码

for /f  耗时: 0 小时 0 分钟 1 秒 47 毫秒
for+改变句柄指向 耗时: 0 小时 0 分钟 0 秒 28 毫秒
findstr 耗时: 0 小时 0 分钟 0 秒 7 毫秒
得出结论:
当需要定向众多内容到文本时
若能用findstr命令完成时,findstr当为首选。
其次“改变句柄指向”效率也是可观的,比直接用 >> 要快5倍左右。
作者: fn74qfdb    时间: 2009-3-5 09:44     标题: 看不懂

看不太懂,我只能说你很牛!
作者: Batcher    时间: 2009-3-5 10:29

  1. ::这样需要启用N次IO
  2. ::十万行代码的测试结果: 0 小时 3 分钟 25 秒 68 毫秒
  3. for /f "delims=" %%a in (c.txt) do >>d.txt echo %%a
  4. ::这样只需要启用一次IO
  5. ::十万行代码的测试结果: 0 小时 0 分钟 2 秒 70 毫秒
  6. (for /f "delims=" %%a in (c.txt) do (
  7.   echo %%a
  8. ))>e.txt
复制代码

作者: zjw767676    时间: 2009-3-5 11:04

高!实在是高!
有个建议:把随风兄以及各位老大的帖子整理一下,可以作为学习P的提高教材!
像这样的帖子在出版的书上是看不到的!!
作者: zqz0012005    时间: 2009-3-5 11:48

标题有点不恰当,应该叫“for /f 解析命令输出的效率”,哈哈。
for /f + 命令  命令所得到的结果是通过管道传递给for的

这个倒不一定。
for /?

...可以用 FOR /F 命令来分析命令的输出。方法是,将
括号之间的 filenameset 变成一个反括字符串。该字符串会
被当作命令行,传递到一个子 CMD.EXE,其输出会被抓进
内存,并被当作文件分析。...


对于 for /f in ('command') 其中command的运行方式是 cmd /c "command"
启动CMD要消耗时间(启动一个外部命令可能耗时都差不多吧,7ms,看机器配置。运行时间则看具体命令了)
但主要的耗时可能花在“输出会被抓进内存,并被当作文件分析。”
同时也说明,输出应该不是通过管道传递给for的。

当然管道也是有一定耗时的,这要从管道的运行机制说起(在verybat上也讨论过,参与的人不多,只得出了粗略的结果)
管道前后的命令,有的要通过CMD启动,所以管道的耗时也大致等于启动CMD等外部命令的时间,管道的传递耗时可能很少。
比如一个1万行的文本,我的CPU 3.00G,内存1G做如下测试:
findstr .* a.txt 用时12ms
type a.txt|findstr .* 用时16ms
作者: 愚无尽    时间: 2009-3-5 13:37

研究的层次很高,水平不赖
作者: 随风    时间: 2009-3-5 20:43     标题: 回复 5楼 的帖子

有一点疑问,如果是抓入内存,那么应该不会与内容多少有关吧?而应该是与内存的大小有关了,比如列子中,匹配5000行就明显慢一些,按你的说法,应该也是只启动一次cmd 啊。
作者: zqz0012005    时间: 2009-3-7 14:08

for /f 解析命令输出,的确是输出越多,速度越慢。
“输出会被抓进内存,并被当作文件分析”,这个的具体机制还不清楚。

for /f 如果直接解析文本,虽然是一次性全部读入到内存中,但速度影响不大。

猜想一下:
直接解析文本时是根据文件大小申请内存一次;但解析命令输出时,由于无法预期输出的大小,可能是边输出边申请内存,类似于动态数组。

[ 本帖最后由 zqz0012005 于 2009-3-7 14:09 编辑 ]
作者: dali    时间: 2009-6-13 12:28

效率这么大区别  以后要注意了




欢迎光临 批处理之家 (http://www.bathome.net/) Powered by Discuz! 7.2