标题: [原创] 探讨批处理代码效率 [打印本页]
作者: netbenton 时间: 2009-5-10 23:24 标题: 探讨批处理代码效率
为了处理一些windows不方便做的事情时,我们不得不选择cmd批处理来解决问题,
以逸待劳,来为我们服务。稍有一点了解批处理的都知道,cmd批处理是功能“强大”,但代码运行速度太慢。
于是问题出来了,本来我们选择用cmd批处理的目的是为了更方便和更快速的,可是
cmd批处理的速度和我们期望目标成了矛与盾的较量。
同一个问题批处理的解决办法,往往有多种,不同算法之间的速度相差非常之大,
为了提高cmd批处理程序,算法很关键,于是,都费尽脑汁,想方设法找一些巧妙的
方法。在注重算法的同时,往往忽略了或误解了最基本的问题:“批处理命令的效率”
其实了解批处理命令的效率,有一个良好的BAT编写习惯,可以在为了好的算法
伤脑筋时,少走弯路。我根据在cn-dos所学到的简单总结了一下,欢迎各位指出错误。
声明:
以下仅为个人观点,有兴趣的就看看,并不代表权威数据,
特此声明,以免对大家有误导!
**********
因为不同的电脑速度相差太大,为了能有个统一标准,以@echo off 执行间为0.20毫秒为标准
**********
1. 预处理与运行速度
批处理的预处理已经有多人讨论过了,可以在论坛搜索“预处理”查看。这里就不详述,
只讨论预处理同效率的问题。
其实每行代码真正执行的时间不长(0.01~0.02毫秒),可是单单运行一行代码时,
最少也要0.19毫秒(如:注释行rem),为什么呢?我认为:这应该是批处理的运行的
“预处理”造成的,批处理最好的地方在这里却成了拌脚石,有没有办法解决这个问题呢?
有,把多行命令合成组执行,批处理对组()内的多行代码一次性预完成的。
()分组包括If () for () 和直接(),和用&并行,都具有同样的效果,能让系统一次性对多
条指令进行同时预处理,一组命令的用时为:一个预处理时长(0.19毫秒)+每条命令的执行时
长(0.01~0.02毫秒),也就是越多条命令合成一组,效率越高。for 的效率高的主要原因应该就在这里。
2. 管道操作与运行速度
管道操作对象有:程序(外部命令,三方工具等),内部命令(其实是命令解释器),
设备(空设备nul,标准输入输出设备con,打印设备prn,存储设备即常用的文件)
标准设备输入输出间的转换并不会占用很多时间,只有管道操作的对象为程序和内部命令时,
才会突然加大时间开销,如:
echo a 0.41毫秒
echo a>>b.txt, 0.30毫秒
echo. a>>b.txt 1,30毫秒 (不知为什么这一点会多用那么多时间)
findstr “aa” a.txt 44.90毫秒
type a.txt |findstr “aa” 99.12毫秒
for /f %%a in (‘set #’) do (rem), 42.70毫秒
echo a|echo b 90.04毫秒
echo aa|findstr .* 88.84毫秒
echo a|sort 89.12毫秒
sort a.txt 45.45毫秒
abcd.com 12.50毫秒 (一个最直接退出的程序,非管道)
可见,程序和命令进行管道操作是需要很大资源的,为什么内部命令进行管道操作也要那么多的时间呢?
如:echo a|echo b
这可能是当前内部命令是不能接收管道数据的,而是调用了另一个命令解释器,再执行其中一个命令,
所以像调用了一个外部程序一样。for /f %%a in (‘command’) do 也应视为一次管道操作,
而使用“|”是进行了双管道操作所以用时多了一半。
3. 子程序调用时,选择文件还是函数
在cmd下,可以使用call :标号 来实现子过程调用,这种方法可以把多个子功能集中到一个BAT文件内,
进行内部调用。这种方法可以减少BAT文件的个数,可是要是考虑到效率问题时,
与调用外部BAT文件相比会是怎么样呢?测试结果:
Call :sub 3.51 毫秒
Call abc.bat 1.28 毫秒
出人意料!怎么外部调用会比内部调用更高效呢???
4. Call 各种方法中的效率问题
Call 的用法有多种,效率如下:
Call echo %%!n!b%% 1.64毫秒
for %%a in (!n!) do echo !%%ab! 0.63毫秒 看来用用for 来取代上句是可以加速度的
Call set aa=%%bb%% 1.47毫秒
Call :sub 3.51 毫秒 (空操作)
Call abc.bat 1.28 毫秒 (空操作)
作者: netbenton 时间: 2009-5-10 23:25 标题: (接一楼)
5. Goto 标号为什么慢呢? 看:
goto :next 0.27毫秒 (并不慢)
:next
其实,单个goto 的速度并不是很慢,慢的原因是goto :标号和标号间不支持组合,
也就是每行组指令最少也要0.20毫秒以上。所以通常不应该用goto :标号 来构造循环体,
如果一定要这样时,应该把循环体内的多行,用()或用 “&”进行组合来减少行数。
通常只用在分支,或跳出for 循环。如果不得不用到时,最好用for来辅助以减少循环次数。
6. 注释与也与速度有关吗?
注释方法有两种.
::号
当发现这以用这种方法后,绝大多数人都喜欢上这种方法,因为觉得它基本上不耗时,
其实并非这样,在运行过程中,系统遇到时相当于处理标号一样的时间:0.05毫秒,
其实只要是一行,就算是空行也要0.05毫秒的时间,也就是基本不用时间。
缺点是不能用在命令行后。也不能用在分组()内
Rem
按道理没有进行作何操作,应该不耗时才对,可是并非如此,单独一行rem要用0.19毫秒,
可见它没有 ::注释高效,是否rem该退休了呢?不,实际并非如此,因为它可以用在行后,
和分组()内,这时它的速度到了0.01~0.02毫秒,这是好多人没有想到的。
7. 外部命令和三方软件的效率问题,文件大少对用时在cmd下相差不是很大,主要是路径的问题,
以直接指明路径最快,在当前路为次,最慢是在%path%路径的最后。三方软件的调用速度是很慢的,
最快操作单个也要12.25毫秒,所以要用到三方的话,尽量在1到3次内完成,不要多次调用三方和外部命令,
特别是不能用在循环体内,除非不得已。这也就是为什么我不想用三方来做界面的原因。
8. 行长与速度,每长30个字符要多用0.01毫秒,因为批处理是一边运行一边解释的,
所以字符串的长短与时间也是有关的。最好是变量名的定义只能要方便看懂,越短越好。
附:(本人对一些命令实测效率,运行1000次所用毫秒数,测试工具请在一楼附件下载)
命令 运行次数 用时(毫秒)
cd.
=== 1000 161
echo.
=== 1000 158
echo.>nul
=== 1000 161
echo.aa>nul
=== 1000 182
echo dd>nul
=== 1000 30
::dir
=== 1000 5
rem dir
=== 1000 19
set aa=aa
=== 1000 21
set /a aa=1
=== 1000 25
set aa=^(3+3^)*1
=== 1000 24
set /a aa=8^^3
=== 1000 25
for /l %%a in (1,1,10) do rem dir
=== 1000 30
for /l %%a in (1,1,100) do set aa=aa
=== 1000 193
for /l %%a in (1,1,10) do set aa=aa
=== 1000 46
if 22==aaa rem dd
=== 1000 21
if 22==aaa set aa=aa
〈〈数据未完,详请看附件〉〉
[ 本帖最后由 netbenton 于 2009-5-11 12:32 编辑 ]
作者: Batcher 时间: 2009-5-10 23:39
慢和快通常是相对的概念,我们通过怎样的例子来说明批处理运行太慢呢?如果说批处理慢的话,什么东西快呢?
这可能是当前内部命令是不能接收管道数据的,而是调用了另一个命令解释器
据某高人说,系统内部是通过临时文件来实现管道的。
关于各个命令的耗时,你的排版好像有点乱,能否重新整理一下。
作者: Batcher 时间: 2009-5-10 23:43
关于命令的耗时,plp626也做过类似的统计:
xp5.1版的单核,2.4G,256M 测试
方法:for /l %%a in (1 1 %1)do %2 结合timediff 函数计算。
命令 效率(次/秒)
--------------------------------------------------------------------------------------------------
echo ... 364.3
echo... >1.txt" 119.3 (只要...是一行,耗时与...长度有关,但差别不明显)
echo ...>nul" 8772.6
call echo ...>nul 1204.7
if 1==2 do ..... 61349.7
@echo off 71428.6
set d=%x% 42553.2
set/a n+=1 27411.8
call 空+goto:eof 917.4
goto循环 1219.5
shift 102040
echo %%a|find >nul 8474.5
echo %%a|findstr >nul 8474.5
for /f ('more') echo >nul 2806.7
for /f ('type') echo >nul 3932.1
for /f ('findstr .*') echo >nul 4007.6
for /f ('find /v ""') echo >nul 2966.5
for /f ( 文件) echo >nul 9807.5
echo.>1.txt 181.7
echo off>1.txt 1666.7
del 1.txt 1961.7
pushd %tmp% 2762.4
cd %tmp% 2808.7
pushd 76923.1
for /r (.)是for /f (dir/ad/s)速度的2倍。
频繁生成临时文件时用echo off>tmp效率最高。
call echo ...会大大降低echo 的速度。
多用if判断语句,与goto:eof会提高效率。
原文地址:http://www.cn-dos.net/forum/viewthread.php?tid=39841
作者: netbenton 时间: 2009-5-11 00:07
re 2,3楼:
命令耗时单,在附件里的排是好的,发上来就变了,有兴趣的人可以下载附件来看。
看了一下,plp626 所测试的数据,是以for /l 来实现执行次数,本人认为有误差,
因为for /l 本身也要耗时,另外在for /l 内是进行代码组合了的,并不能了解到因预处理所花的时间
就拿shift 这个命令来说吧,其单条命令应为0.19毫秒,相当于一行rem行的时间,而在组合里面,它用时则仅为0.01毫秒。
而我的方法是,把一千行命令写到一个临时BAT,并在这一千行代码的头尾,加上取数时间命令set aa=%time%
然后调用这个临时BAT来取得运行一千次命令前后的时间。所以误差较小。
[ 本帖最后由 netbenton 于 2009-5-11 00:16 编辑 ]
作者: 随风 时间: 2009-5-11 00:09
把多行命令合成组执行,批处理对组()内的多行代码一次性预完成的。
()分组包括If () for () 和直接(),和用&并行
这倒是不知道,学习了。。。。。
对于call我现在简直已经到了痛恨的地步了,不到万不得已是绝不用它的。
[ 本帖最后由 随风 于 2009-5-11 00:11 编辑 ]
作者: 随风 时间: 2009-5-11 00:22
又一发现分别对 ehco. 和 echo/ 、echo\ 、echo= 、作了测试,发现除了点以外,其余的耗时都和echo 加 空格一样。看来以后要改掉用 echo点的习惯才行了。
:loop2
echo=a>nul
echo=a>nul
echo=a>nul
echo=a>nul
echo=a>nul
set /a n+=1
if !n! lss 1000 goto loop2
echo点 耗时 23秒,而其它的 5-6秒。。
作者: netbenton 时间: 2009-5-11 00:30
原帖由 随风 于 2009-5-11 00:22 发表
又一发现分别对 ehco. 和 echo/ 、echo\ 、echo= 、作了测试,发现除了点以外,其余的耗时都和echo 加 空格一样。看来以后要改掉用 echo点的习惯才行了。
:loop2
echo=a>nul
echo=a>nul
echo=a>nul
echo=a>nul ...
原来只有点号才会这样呀,我还以为是显示一行多用的时间呢,
这点我倒没有发现!
测试了一下,用echo=dd和echo,dd及echo;dd这三个用时最少为: 0.28毫秒
[ 本帖最后由 netbenton 于 2009-5-11 00:39 编辑 ]
作者: 随风 时间: 2009-5-11 00:35
不知是否和点表示路径有关?
猜想。。。。。
作者: 随风 时间: 2009-5-11 01:07
::号
其实并非这样,在运行过程中,系统遇到时相当于处理标号一样的时间:0.05毫秒,
Rem
按道理没有进行作何操作,应该不耗时才对,可是并非如此,单独一行rem要用0.019毫秒,
可见它没有 ::注释高效
这句是不是写错了?
.
Call :sub 1.28毫秒
Call abc.bat 3.51毫秒
出人意料!怎么外部调用会比内部调用更高效呢???
还有这句,也写反了吧?
.
Call :abc.bat 1.28毫秒 (空操作)
还有这句是不是多了个冒号?
作者: netbenton 时间: 2009-5-11 07:47 标题: re 楼上
确实写错,已经在一楼更正.
谢谢指正。
作者: netbenton 时间: 2009-6-2 01:31 标题: 在批处理程序设计中,取字符长度最高效的方法
在批处理程序设计中,取字符长度最高效的方法- @echo off&setlocal enabledelayedexpansion
- set aav=abcdefghijklmnopqrstuvwxyz不够的话文字也可以abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
- set 最大长度=80
- for /l %%a in (0,1,%最大长度%) do (
- set len!aav:~%%a,1!=%%a
- set vva=!aav:~%%a,1!!vva!
- )
- ::前面初始定义一次,后面可以任意使用,只要两个set 就可以取得字符串长度。
- ::但前提是,你要知道需计算的字符串中,最长字符串的长度。
- ::效率可想而知了!
-
- set str1=12345678
- set str2=abcdefghijklmn
-
- set var1=!str1!!vva!
- set var2=!str2!!vva!
- set/a var1=len!var1:~%最大长度%,1!,var2=len!var2:~%最大长度%,1!
- echo !str1! 长度为:!var1!
- echo !str2! 长度为:!var2!
- pause
复制代码
作者: 随风 时间: 2009-6-2 01:44
re 12 楼
效率确实高,打破了以前讨论过的 15 位的限制,但应该还有前提,就是 avv 的值不能有重复的吧?
作者: Batcher 时间: 2009-6-2 02:54 标题: 回复 13楼 的帖子
“15 位的限制”是指什么?能否给个链接让我学习下?
作者: zqz0012005 时间: 2009-6-2 03:39
什么时候出了这么一个好帖?竟然没注意到。
计算字符串长度,利用临时文件和xcopy,也有一个不错的办法- @echo off
- set "str=Hello, bat! %%<^_^>%%""
- setlocal enabledelayedexpansion
- set str2=!str:y=-!
- set str2=!str2:n=-!
- set str2=!str2:a=-!
- echo;!str2!n>str.tmp
- goto:next 用for解析命令输出会降低效率,干脆用临时文件
- for /f %%a in ('
- xcopy /-y %SystemRoot%\notepad.exe %SystemRoot%\explorer.exe ^<str.tmp ^| find /i /c "%SystemRoot%\explorer.exe"
- ') do set strlen=%%a-1
- :next
- xcopy /-y %SystemRoot%\notepad.exe %SystemRoot%\explorer.exe <str.tmp | find /i /c "%SystemRoot%\explorer.exe" > strlen.tmp
- set /p strlen=<strlen.tmp
- set /a strlen-=1
- echo;!str!&echo/&echo 上面的字符串长度为:!strlen!
- del str.tmp strlen.tmp
- pause
复制代码
作者: 随风 时间: 2009-6-2 03:49 标题: 回复 14楼 的帖子
链接一时找不到了,但还记得代码,是技术组的一位成员发的,在只运行一次set的情况下,最高只能判断15位字符。
:- @echo off
- set "var=12345678"
- set "str=%var%fedcba9876543210"
- set /a max=0x%str:~15,1%
- echo %var% 有 %max% 位
- pause
复制代码
[ 本帖最后由 随风 于 2009-6-2 18:12 编辑 ]
作者: 随风 时间: 2009-6-2 03:59
找到了
523066680 在顶楼给出的9位的算法
http://www.bathome.net/viewthrea ... hlight=%D7%D6%B7%FB
15楼 tireless兄将其扩展到了15位
http://www.bathome.net/viewthrea ... =%A1%BE%C1%B7%CF%B0
作者: Batcher 时间: 2009-6-2 04:01 标题: 回复 16楼 的帖子
原来是“在只运行一次set的情况下”啊,呵呵,了解了。
作者: zqz0012005 时间: 2009-6-2 06:39 标题: 回复 15楼 的帖子
其实不用临时文件的话,效率也不是很低,而且更简洁一些。- @echo off
- set "str=例如:Hello, bat! %%<^_^>%%""
- setlocal enabledelayedexpansion
- set str2=!str:y=-!
- set str2=!str2:n=-!
- set str2=!str2:a=-!
- for /f %%a in ('
- set str2^|xcopy /-y %SystemRoot%\notepad.exe %SystemRoot%\explorer.exe ^| find /i /c "%SystemRoot%\explorer.exe"
- ') do set /a strlen=%%a-9
- echo;!str!&echo/&echo 上面的字符串长度为:!strlen!
- pause
复制代码
作者: netbenton 时间: 2009-6-2 13:15
re 13L
是的 a v v 变量字符不能有重复。但是全半角字符都可以,只要不重复任意使用。
此法在计算 子串 在 字符串 中的位置很实用。
re 19L
用到临时文件的话这样最简单(按字节计算的):
set str=任意字符串。
set /p=!str!<nul>len.temp
for %%a in (len.temp) do (echo !str! 长度为:%%~za)
del/q len.temp
作者: zqz0012005 时间: 2009-6-2 14:18 标题: 回复 20楼 的帖子
计算字节数方法较多。我的是计算字符个数。
作者: plp626 时间: 2009-6-5 21:23
为了充分保证效率千万不要使用下面两个命令
call echo.....
call set a=...
万不得已要call set n=数字
一定要call set/a n=数字 //这个效率还是很高的是set/a 一半的效率
我发现call set a=... 的执行效率比call子过程还要低10倍以上!
作者: tireless 时间: 2009-6-7 12:28
原帖由 netbenton 于 2009-6-2 01:31 发表
在批处理程序设计中,取字符长度最高效的方法@echo off&setlocal enabledelayedexpansion
set aav=abcdefghijklmnopqrstuvwxyz不够的话文字也可以abcdefghijklmnopqrstuvwxyzABCDEF ...
不用把aav倒过来,可以这样:
set str1=12345678
set var1=!aav:~,81!!str1!
set /a var1=len!o:~-81,1!
[ 本帖最后由 tireless 于 2009-6-7 12:29 编辑 ]
作者: cjiabing 时间: 2009-6-7 13:54
我觉得find和findstr是最慢的,好像没都耐心试过这两个查找东东的,发现dir和for比她两还快!~当然for的目录多了它也跑不动,多几个for更是慢。
关于临时文件,有时候你为了看运行结果得用pause和>>,似乎那样代码就没效率了
作者: neorobin 时间: 2009-12-11 06:01 标题: 回复 12楼 的帖子
确实是很好的算法, 缺憾是当最大长度很大时, 也会需要很大的变量空间, len后面的标识符可用辅助代码生成无重复的序列
和 23 楼的想法一样, vva 变量是多余的, 我的改法更简明一些, 做个减法就行了- @echo off&setlocal enabledelayedexpansion
- set aav=abcdefghijklmnopqrstuvwxyz不够的话文字也可以abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
- set 最大长度=80
- for /l %%a in (0,1,%最大长度%) do set /a len!aav:~%%a,1!=最大长度-%%a
- ::前面初始定义一次,后面可以任意使用,只要两个set 就可以取得字符串长度。
- ::但前提是,你要知道需计算的字符串中,最长字符串的长度。
- ::效率可想而知了!
-
- set str1=12345678
- set str2=abcdefghijklmn
-
- set var1=!str1!!aav!
- set var2=!str2!!aav!
- set/a var1=len!var1:~%最大长度%,1!,var2=len!var2:~%最大长度%,1!
- echo !str1! 长度为:!var1!
- echo !str2! 长度为:!var2!
- pause
复制代码
[ 本帖最后由 neorobin 于 2009-12-11 06:14 编辑 ]
作者: 523066680 时间: 2009-12-11 08:36
路过,把老东西不被看好的再贴一次,不要BS我,只因为很久没说话了
- @echo off
- setlocal enabledelayedexpansion
- set str=cn-dos.net
- set st2=Str%str%11111111111111111111111111111111111111111111111111111111111111111111111111111111
- set "st2=%st2:~0,83%"
- set n=!st2:Str%str%=-0!
- set /a n=80 %n:1=-1%
- echo,%n%
- pause
- ::个数可以是0 范围是0~80 的普通字符串。
复制代码
[ 本帖最后由 523066680 于 2009-12-11 08:37 编辑 ]
作者: newswan 时间: 2021-8-16 17:02
回复 26# 523066680
很好的算法,简单明了
作者: yakeyun 时间: 2021-8-21 18:50
回复 1# netbenton
echo. a>>b.txt 1,30毫秒 (不知为什么这一点会多用那么多时间)
之所以这里时间会多很多,是由于加了点后,相当于创建的文件前面要多一个空格符号。如果在for命令里面,数据多了,将是很恐怖的一个事情,会每一行前面加一个空格符号。
作者: fzp070 时间: 2022-8-21 23:25
感谢分享!学习学习
作者: qixiaobin0715 时间: 2022-8-23 09:21
本帖最后由 qixiaobin0715 于 2022-8-23 09:46 编辑
看了大佬们的代码,真是受益匪浅。
自己也提供一个思路,二分法。下面字符长度限定在1024之内,可根据具体情况自行调整。
由于水平有限,效率不在考虑范围之内:- @echo off
- setlocal enabledelayedexpansion
- set Var=123456789
- set a=512 256 128 64 32 16 8 4 2 1
- if defined Var (
- set n=1
- for %%i in (%a%) do (
- if not "!Var:~%%i!"=="" (
- set Var=!Var:~%%i!
- set /a n+=%%i
- )
- )
- echo,!n!
- ) else (
- echo,Var is not defined
- )
- pause
复制代码
作者: aloha20200628 时间: 2022-8-23 12:02
看这个十年前的老帖 http://www.bathome.net/thread-11799-1-1.html 可见当年批处理计算字符串长度的"技法峰值"
在此分享源网站 https://www.dostips.com 的这段经典代码(见以下代码段),其内还有两枚技术硬核》
一。句式 set "str=a!%~1!" 提高形参 %1 的容错率,一网打尽键盘所有可见字符
二。句式 (endlocal ... set /a %~2=%len%) 令局部变量亡前可续命给全局变量
附加几行代码》针对经典代码的测试/用法- @echo off
- :[Loop] //测试代码 备注》调用子过程的形参须是变量名
- set "str=" &set/p str="输入一个字符串获取其长度:"
- if not defined str exit/b
- (call :strLen str sL)
- echo,长度=%sL%
- goto[Loop]
-
- :: 分享计算字符串长度的经典代码如下》
- :: string [in] - variable name containing the string being measured for length
- :: len [out] - variable to be used to return the string length
- :: Many thanks to 'sowgtsoi', but also 'jeb' and 'amel27' dostips forum users helped making this short and efficient
- :: Created 20081122,changed 20101116,source https://www.dostips.com
- :strLen string len -- returns the length of a string
- ( setlocal enabledelayedexpansion
- set "str=a!%~1!" &rem keep the a up front to ensure we get the length and not the upper bound,it also avoids trouble in case of empty string
- set "len=0"
- for /l %%a in (12,-1,0) do (
- set /a "len|=1<<%%a"
- for %%b in (!len!) do if "!str:~%%b,1!"=="" set /a "len&=~1<<%%a"
- )
- )
- ( endlocal & rem return values
- if "%~2" neq "" set /a %~2=%len%
- )
- exit /b
复制代码
作者: qixiaobin0715 时间: 2022-8-23 14:25
回复 31# aloha20200628
谢谢提供链接,内容确实精彩!!!
作者: ANSL 时间: 2022-9-6 19:53
回复 31# aloha20200628
谢谢,正想找这种代码呢!
作者: dos-a 时间: 2023-4-3 13:29
回复 31# aloha20200628
大佬,最近我又看到了Batcher大佬的多行回退,但是并未在win10中实现,不知为什么,
作者: gudou 时间: 2024-4-29 16:43
多年后的膜拜……
欢迎光临 批处理之家 (http://www.bathome.net/) |
Powered by Discuz! 7.2 |