【发布时间】:2019-04-21 03:00:01
【问题描述】:
我编写了一个小批处理文件,将每行包含八个 16 位十六进制值的文件转换为包含八个十进制值的 CSV 文件。
输入数据文件是嵌入式设备的 ADC 值的捕获输出,以 ASCII 格式的八个十六进制值的数据包的形式发送,并通过 RS-232 回车和换行到 PC,然后在 PC 上简单地捕获到文件中。输入数据文件中的一行是这样的:
000A002D0044008B0125018C01F40237
该行的 CSV 文件是:
10,45,68,139,293,396,500,567
批处理文件有效,但完成转换需要几分钟,这让我感到震惊。我预计 Windows 命令处理器需要几秒钟来完成这项任务,而用 C 或 C++ 编写的控制台应用程序可以在几毫秒内完成。但是对于一个小于 512 KiB 的数据文件来说,几分钟的执行时间绝对不是我所期望的。
因此,我进一步研究了这个问题,使用四种不同的方法创建批处理文件,以从具有十六进制值的数据文件创建具有十进制值的 CSV 文件。
下面附上测试四种方法的完整批处理文件以及我的测试结果。
我知道使用子例程的前两种方法比在一个循环中进行转换的后两种方法慢得多,每次循环迭代时分别将每个 CSV 行输出到文件中一次 FOR 由于调用子程序而导致的循环导致cmd.exe 完成了几个额外的步骤,这些步骤总共花费了大量时间来调用子程序数千次。
但我真的不明白为什么使用 GOTO 循环的第一种方法比使用几乎相同的两条命令行的 FOR 循环慢大约六倍。
方法一的批处理文件代码:
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "DelDataFile="
set "DataFile=%TEMP%\HexValues.dat"
if not exist "%DataFile%" set "DelDataFile=1" & echo 000A002D0044008B0125018C01F40237>"%DataFile%"
for /F "usebackq delims=" %%I in ("%DataFile%") do call :ConvertLine "%%I"
if defined DelDataFile del "%DataFile%"
endlocal
goto :EOF
:ConvertLine
set "DataLine=%~1"
set "AllValues="
set "StartColumn=0"
:NextValue
set /A Value=0x!DataLine:~%StartColumn%,4!
set "AllValues=%AllValues%,%Value%"
set /A StartColumn+=4
if not %StartColumn% == 32 goto NextValue
echo %AllValues:~1%
goto :EOF
方法二的批处理文件代码:
@echo off
setlocal EnableExtensions EnableDelayedExpansion
set "DelDataFile="
set "DataFile=%TEMP%\HexValues.dat"
if not exist "%DataFile%" set "DelDataFile=1" & echo 000A002D0044008B0125018C01F40237>"%DataFile%"
for /F "usebackq delims=" %%I in ("%DataFile%") do call :LineConvert "%%I"
if defined DelDataFile del "%DataFile%"
endlocal
goto :EOF
:LineConvert
set "DataLine=%~1"
set "AllValues="
for /L %%J in (0,4,28) do (
set /A Value=0x!DataLine:~%%J,4!
set "AllValues=!AllValues!,!Value!"
)
echo !AllValues:~1!
goto :EOF
而且我在运行测试时还发现自己找出原因,方法 1 在使用电池运行的 PC 上比在插入电源时要长 5 到 10 秒。
问题:
与方法 2 使用的 FOR 循环相比,方法 1 使用的 GOTO 循环执行速度慢得多的原因是什么以及为什么方法 1 依赖于PC的电源是什么?
这是用于比较不同方法的整个批处理文件:
@echo off
setlocal EnableExtensions EnableDelayedExpansion
cls
set "TestRuns=5"
set "DelDataFile="
set "DataFile=%TEMP%\HexValues.dat"
if exist "%DataFile%" goto InitMethod1
set "DelDataFile=1"
echo Creating data file which takes some seconds, please wait ...
setlocal
set "HexDigits=0123456789ABCDEF"
set "DataLine="
(for /L %%I in (0,1,32767) do (
set /A "Digit1=(%%I >> 12) %% 16"
set /A "Digit2=(%%I >> 8) %% 16"
set /A "Digit3=(%%I >> 4) %% 16"
set /A "Digit4=%%I %% 16"
set "HexValue="
for %%J in (!Digit1! !Digit2! !Digit3! !Digit4!) do set "HexValue=!HexValue!!HexDigits:~%%J,1!"
set "DataLine=!DataLine!!HexValue!"
set /A "ValuesPerLine=%%I %% 8"
if !ValuesPerLine! == 7 (
echo !DataLine!
set "DataLine="
)
))>"%DataFile%"
endlocal
echo/
:InitMethod1
call :MethodInit 1
:RunMethod1
set /A TestRun+=1
set "CSV_File=%TEMP%\Values%Method%_%TestRun%.csv"
del "%CSV_File%" 2>nul
call :GetTime StartTime
for /F "usebackq delims=" %%I in ("%DataFile%") do call :ConvertLine "%%I"
call :OutputTime
if %TestRun% LSS %TestRuns% goto RunMethod1
call :MethodResults
goto InitMethod2
:ConvertLine
set "DataLine=%~1"
set "AllValues="
set "StartColumn=0"
:NextValue
set /A Value=0x!DataLine:~%StartColumn%,4!
set "AllValues=%AllValues%,%Value%"
set /A StartColumn+=4
if not %StartColumn% == 32 goto NextValue
>>"%CSV_File%" echo %AllValues:~1%
goto :EOF
:InitMethod2
call :MethodInit 2
:RunMethod2
set /A TestRun+=1
set "CSV_File=%TEMP%\Values%Method%_%TestRun%.csv"
del "%CSV_File%" 2>nul
call :GetTime StartTime
for /F "usebackq delims=" %%I in ("%DataFile%") do call :LineConvert "%%I"
call :OutputTime
if %TestRun% LSS %TestRuns% goto RunMethod2
call :MethodResults
goto InitMethod3
:LineConvert
set "DataLine=%~1"
set "AllValues="
for /L %%J in (0,4,28) do (
set /A Value=0x!DataLine:~%%J,4!
set "AllValues=!AllValues!,!Value!"
)
echo !AllValues:~1!>>"%CSV_File%"
goto :EOF
:InitMethod3
call :MethodInit 3
:RunMethod3
set /A TestRun+=1
set "CSV_File=%TEMP%\Values%Method%_%TestRun%.csv"
del "%CSV_File%" 2>nul
call :GetTime StartTime
for /F "usebackq delims=" %%I in ("%DataFile%") do (
set "DataLine=%%I"
set "AllValues="
for /L %%J in (0,4,28) do (
set /A Value=0x!DataLine:~%%J,4!
set "AllValues=!AllValues!,!Value!"
)
echo !AllValues:~1!>>"%CSV_File%"
)
call :OutputTime
if %TestRun% LSS %TestRuns% goto RunMethod3
call :MethodResults
goto InitMethod4
:InitMethod4
call :MethodInit 4
:RunMethod4
set /A TestRun+=1
set "CSV_File=%TEMP%\Values%Method%_%TestRun%.csv"
del "%CSV_File%" 2>nul
call :GetTime StartTime
(for /F "usebackq delims=" %%I in ("%DataFile%") do (
set "DataLine=%%I"
set "AllValues="
for /L %%J in (0,4,28) do (
set /A Value=0x!DataLine:~%%J,4!
set "AllValues=!AllValues!,!Value!"
)
echo !AllValues:~1!
))>>"%CSV_File%"
call :OutputTime
if %TestRun% LSS %TestRuns% goto RunMethod4
call :MethodResults
goto EndBatch
:GetTime
for /F "tokens=2 delims==." %%I in ('%SystemRoot%\System32\wbem\wmic.exe OS GET LocalDateTime /VALUE') do set "%1=%%I"
goto :EOF
:MethodInit
set "Method=%1"
echo Test runs with method %Method%
echo -----------------------
echo/
set "TestRun=0"
set "TotalTime=0"
goto :EOF
:MethodResults
set /A AverageTime=TotalTime / TestRun
echo Method %Method% total time: %TotalTime% seconds
echo Method %Method% average time: %AverageTime% seconds
echo/
goto :EOF
:OutputTime
call :GetTime EndTime
set /A StartTime=(1%StartTime:~8,2% - 100) * 3600 + (1%StartTime:~10,2% - 100) * 60 + 1%StartTime:~12,2% - 100
set /A EndTime=(1%EndTime:~8,2% - 100) * 3600 + (1%EndTime:~10,2% - 100) * 60 + 1%EndTime:~12,2% - 100
set /A DiffTime=EndTime - StartTime
set /A TotalTime+=DiffTime
echo Method %Method% run %TestRun% time: %DiffTime% seconds
goto :EOF
:EndBatch
if defined DelDataFile del "%DataFile%"
del /Q "%TEMP%\Values?_*.csv"
endlocal
它首先在文件夹中为临时文件创建一个十六进制值递增的数据文件,这些文件已经花费了几秒钟。请注释此批处理文件的倒数第二个命令行以保留该文件,以防多次运行此批处理文件或对此文件感兴趣。
然后它运行五次从数据文件读取十六进制值并将十进制值写入CSV文件的四种方法,并将测试结果打印到控制台分别处理STDOUT。
最后,它会删除所有在文件夹中创建的所有 CSV 文件,这些文件都是具有相同内容的临时文件。请在最后一个命令行上发表评论,以保持这些 CSV 文件对这些文件感兴趣。
这个批处理文件由我在两个笔记本上执行了四次。
以下是在装有 Intel Core Duo P8400、2.26 GHz 和 2 GiB RAM、7200 rpm 硬盘、运行 Windows XP x86 并插入电源的笔记本电脑上首次运行的结果:
Test runs with method 1
-----------------------
Method 1 run 1 time: 51 seconds
Method 1 run 2 time: 51 seconds
Method 1 run 3 time: 51 seconds
Method 1 run 4 time: 52 seconds
Method 1 run 5 time: 51 seconds
Method 1 total time: 256 seconds
Method 1 average time: 51 seconds
Test runs with method 2
-----------------------
Method 2 run 1 time: 9 seconds
Method 2 run 2 time: 9 seconds
Method 2 run 3 time: 9 seconds
Method 2 run 4 time: 8 seconds
Method 2 run 5 time: 9 seconds
Method 2 total time: 44 seconds
Method 2 average time: 9 seconds
Test runs with method 3
-----------------------
Method 3 run 1 time: 3 seconds
Method 3 run 2 time: 3 seconds
Method 3 run 3 time: 4 seconds
Method 3 run 4 time: 3 seconds
Method 3 run 5 time: 3 seconds
Method 3 total time: 16 seconds
Method 3 average time: 3 seconds
Test runs with method 4
-----------------------
Method 4 run 1 time: 3 seconds
Method 4 run 2 time: 2 seconds
Method 4 run 3 time: 2 seconds
Method 4 run 4 time: 2 seconds
Method 4 run 5 time: 2 seconds
Method 4 total time: 11 seconds
Method 4 average time: 2 seconds
方法 2 比方法 1 快 5.67 倍。方法 3 和 4 甚至比方法 2 还要快,但这是我的预期。方法 3 和 4 所需的 2 秒和 3 秒大部分来自 WMIC 命令,以获取区域独立格式的本地日期和时间。
以下是在同一台计算机上第二次运行与第一次运行时的结果,不同之处在于在充满电的电池上运行 PC:
Test runs with method 1
-----------------------
Method 1 run 1 time: 63 seconds
Method 1 run 2 time: 61 seconds
Method 1 run 3 time: 61 seconds
Method 1 run 4 time: 61 seconds
Method 1 run 5 time: 61 seconds
Method 1 total time: 307 seconds
Method 1 average time: 61 seconds
Test runs with method 2
-----------------------
Method 2 run 1 time: 11 seconds
Method 2 run 2 time: 10 seconds
Method 2 run 3 time: 10 seconds
Method 2 run 4 time: 10 seconds
Method 2 run 5 time: 10 seconds
Method 2 total time: 51 seconds
Method 2 average time: 10 seconds
Test runs with method 3
-----------------------
Method 3 run 1 time: 3 seconds
Method 3 run 2 time: 4 seconds
Method 3 run 3 time: 3 seconds
Method 3 run 4 time: 4 seconds
Method 3 run 5 time: 3 seconds
Method 3 total time: 17 seconds
Method 3 average time: 3 seconds
Test runs with method 4
-----------------------
Method 4 run 1 time: 2 seconds
Method 4 run 2 time: 2 seconds
Method 4 run 3 time: 2 seconds
Method 4 run 4 time: 2 seconds
Method 4 run 5 time: 2 seconds
Method 4 total time: 10 seconds
Method 4 average time: 2 seconds
可以看出,对于方法 2 到 4,处理时间只增加了一点点。但是方法 1 的处理时间增加了 10 秒,所以这个解决方案现在比方法 2 慢了大约 6.10 倍。我不知道为什么方法 1 的处理时间取决于电源类型。
以下是在配备 Intel Core Duo T9600、2.80 GHz 和 4 GiB RAM、运行 Windows 7 x64 并插入电源的 SSD 的笔记本上首次运行的结果:
Test runs with method 1
-----------------------
Method 1 run 1 time: 91 seconds
Method 1 run 2 time: 88 seconds
Method 1 run 3 time: 77 seconds
Method 1 run 4 time: 77 seconds
Method 1 run 5 time: 78 seconds
Method 1 total time: 411 seconds
Method 1 average time: 82 seconds
Test runs with method 2
-----------------------
Method 2 run 1 time: 11 seconds
Method 2 run 2 time: 16 seconds
Method 2 run 3 time: 16 seconds
Method 2 run 4 time: 14 seconds
Method 2 run 5 time: 16 seconds
Method 2 total time: 73 seconds
Method 2 average time: 14 seconds
Test runs with method 3
-----------------------
Method 3 run 1 time: 6 seconds
Method 3 run 2 time: 4 seconds
Method 3 run 3 time: 4 seconds
Method 3 run 4 time: 4 seconds
Method 3 run 5 time: 6 seconds
Method 3 total time: 24 seconds
Method 3 average time: 4 seconds
Test runs with method 4
-----------------------
Method 4 run 1 time: 4 seconds
Method 4 run 2 time: 3 seconds
Method 4 run 3 time: 5 seconds
Method 4 run 4 time: 4 seconds
Method 4 run 5 time: 4 seconds
Method 4 total time: 20 seconds
Method 4 average time: 4 seconds
有趣的是,使用更强大的硬件执行批处理文件在 Windows 7 x64 上比在 Windows XP x86 上花费更多时间。但对我来说更有趣的是,方法 2 比方法 1 快 5.86 倍,这仅仅是因为使用了 FOR 而不是 GOTO 循环。
为了完整起见,第四次在同一台计算机上运行的结果与第三次运行的结果不同,在充满电的电池上运行 PC:
Test runs with method 1
-----------------------
Method 1 run 1 time: 97 seconds
Method 1 run 2 time: 91 seconds
Method 1 run 3 time: 90 seconds
Method 1 run 4 time: 81 seconds
Method 1 run 5 time: 77 seconds
Method 1 total time: 436 seconds
Method 1 average time: 87 seconds
Test runs with method 2
-----------------------
Method 2 run 1 time: 12 seconds
Method 2 run 2 time: 16 seconds
Method 2 run 3 time: 17 seconds
Method 2 run 4 time: 16 seconds
Method 2 run 5 time: 13 seconds
Method 2 total time: 74 seconds
Method 2 average time: 14 seconds
Test runs with method 3
-----------------------
Method 3 run 1 time: 6 seconds
Method 3 run 2 time: 6 seconds
Method 3 run 3 time: 5 seconds
Method 3 run 4 time: 5 seconds
Method 3 run 5 time: 5 seconds
Method 3 total time: 27 seconds
Method 3 average time: 5 seconds
Test runs with method 4
-----------------------
Method 4 run 1 time: 4 seconds
Method 4 run 2 time: 4 seconds
Method 4 run 3 time: 5 seconds
Method 4 run 4 time: 4 seconds
Method 4 run 5 time: 4 seconds
Method 4 total time: 21 seconds
Method 4 average time: 4 seconds
方法 3 到 4 的执行时间与插入电源的第三次运行相比没有太大差异。但是方法 1 的执行时间增加了大约 5 秒,因此方法 1 是 6.21 倍比方法2慢。
我真的很想知道为什么方法 1 比方法 2 慢得多,而且还取决于电源类型。
由于 Windows 文件缓存,硬盘活动 LED 在所有测试运行中很少闪烁。
【问题讨论】:
-
因为 FOR 命令和包含它的所有行都从文件中读取一次,然后完整的解析代码会被执行几次从记忆里。另一方面,a GOTO 只是转移执行,因此循环中的所有行都以通常的批处理文件方式执行:打开文件,读取行,关闭文件,解析行,执行行。 ..
-
在完整脚本顶部带有
:top标签,设置一个计数器变量并在32767处退出。添加goto :bottom。在底部添加:bottom和goto :top。执行它需要 25 秒,因为它必须读取整个脚本的每一行 32767 次,向下搜索:bottom标签。 -
@Aacini 你在上面写的绝对正确。这可以在批处理文件执行期间由 Sysinternals Process Monitor 记录的文件系统访问中看到。不使用 GOTO 和 CALL 处理
HexValues.dat中的数据的解决方案不会在处理@ 中的所有行时对HexValues.dat和批处理文件进行任何文件访问987654339@ 与 GOTO 和 CALL 的解决方案相比,批处理文件非常频繁地逐行读取。所以使用延迟扩展的 FOR 循环肯定比使用 GOTO 或 CALL 更好。
标签: windows batch-file cmd