优化AutoHotkey v1和v2脚本速度的终极指南

本文吸收了AutoHotkey论坛sashaatx、WAZAAAAA的测试成果,由河许人补充完善,2024年3月28日第一次修改

在编写AutoHotkey(AHK)脚本时,优化脚本的速度对于提高性能至关重要。无论是在AHK v1还是AHK v2版本,都有一些技巧和方法可以帮助您加速脚本的执行。本指南将详细介绍如何优化AutoHotkey v1和v2版本脚本的速度,涵盖了两个版本的不同优化技巧。

一、AutoHotkey v1的优化技巧

(一)通用优化
只需在任何 AHK 脚本的开头复制此内容即可使用性能优化:

;OPTIMIZATIONS START
#NoEnv
#MaxHotkeysPerInterval 99000000
#HotkeyInterval 99000000
#KeyHistory 0
ListLines Off
Process, Priority, , A
SetBatchLines, -1
SetKeyDelay, -1, -1
SetMouseDelay, -1
SetDefaultMouseSpeed, 0
SetWinDelay, -1
SetControlDelay, -1
SendMode Input
DllCall("ntdll\ZwSetTimerResolution","Int",5000,"Int",1,"Int*",MyCurrentTimerResolution) ;setting the Windows Timer Resolution to 0.5ms, THIS IS A GLOBAL CHANGE
;OPTIMIZATIONS END

;YOUR SCRIPT GOES HERE
DllCall("Sleep","UInt",1) ;I just slept exactly 1ms!
DllCall("ntdll\ZwDelayExecution","Int",0,"Int64*",-5000) ;you can use this to sleep in increments of 0.5ms if you need even more granularity 
  • #NoEnv建议用于所有脚本,它会禁用环境变量。
  • #MaxHotkeysPerInterval以及#HotkeyInterval,建议设置一个很大的值。
  • #KeyHistory和ListLines是用于“记录按键”的函数。建议禁用它们,因为它们仅用于调试目的。
  • 建议设置更高的优先权到 Windows 程序应该提高其性能。
  • 默认情况下,SetBatchLines使脚本每行休眠约 10-15.6 毫秒。建议将 -1 设置为不休眠(但请记住在循环中至少包含一个休眠,以避免大量 CPU 浪费!这很重要)。
  • 建议设置SetKeyDelay、SetMouseDelay(设置Mouse延迟)和SetDefaultMouseSpeed为 -1 可以提高 SendEvent 的速度,以防 SendInput 不可用并回退到 SendEvent。
  • SetWinDelay和SetControlDelay可能会影响性能,具体取决于脚本。
  • SendInput是最快的发送方法。SendEvent(默认的)排在第二位,SendPlay排在第三位(尽管它是最兼容的)。SendInput 不服从 SetKeyDelay、SetMouseDelay、SetDefaultMouseSpeed;在该模式下,击键之间没有延迟。
;OPTIMIZATIONS START
#NoEnv
#MaxHotkeysPerInterval 99000000
#HotkeyInterval 99000000
#KeyHistory 0
ListLines Off
Process, Priority, , A
SetBatchLines, -1
SetKeyDelay, -1, -1
SetMouseDelay, -1
SetDefaultMouseSpeed, 0
SetWinDelay, -1
SetControlDelay, -1
SendMode Input
DllCall("ntdll\ZwSetTimerResolution","Int",5000,"Int",1,"Int*",MyCurrentTimerResolution) ;setting the Windows Timer Resolution to 0.5ms, THIS IS A GLOBAL CHANGE
;OPTIMIZATIONS END
BenchmarkLoopCount := 1000
DllCall("QueryPerformanceFrequency", "Int64*", Frequency)
StartTime := A_TickCount
DllCall("QueryPerformanceCounter", "Int64*", CounterBefore)


;BENCHMARK START
Loop,%BenchmarkLoopCount%
{
	SendEvent {Pause} ;run the script 3 times by switching SendEvent to SendInput and SendPlay to benchmark the 3 different Send methods
}
;BENCHMARK END


DllCall("QueryPerformanceCounter", "Int64*", CounterAfter)
ElapsedTime := A_TickCount - StartTime
MsgBox % "Loops:`n" BenchmarkLoopCount "`n`n" "TickCount:`n" ElapsedTime " ms`n`n" "QueryPerformanceCounter:`n" . (CounterAfter - CounterBefore)*1000/Frequency . " ms"
  • 有据可查的是sleeep不精确。但是,如果您先更改 Windows 计时器分辨率,然后通过 DllCall 休眠,则不会浪费 CPU!

注意:
– 此 Sleep 方法的一个主要缺点是它会冻结整个 AHK 线程,因此例如,当 AHK 长时间冻结睡眠时,热键将被忽略,并且 SetTimer 将被延迟
– NtSetTimerResolution/ZwSetTimerResolution 是一个未记录的 Windows 函数
– 将计时器设置为低于最小值的任何(正)值会将其设置为最小值。最大
值的行为相同 – 最大(最精确)计时器通常为 0.5 毫秒(因操作系统版本而异)
– 最小(最不精确)计时器通常为 15.6 毫秒(因操作系统版本而异)
– 当前计时器因计算机而异,并且可以不断变化。您可以使用 OpenTimerResolution 来监控这一点(有无数其他程序可以显示您的 Windows Timer Resolution,但只有这个程序会不断更新其界面以反映事实)
– 如果多个程序请求不同的计时器分辨率,则最精确的程序将优先(这记录在 timeBeginPeriod 中
– 使用 NtDelayExecution/ZwDelayExecution,如果您当前的定时器分辨率为 0.5ms,则即使您可以在其中输入任何数字,它也只会以 0.5ms 的增量休眠。所以-5000=0.5ms,-10000=1ms,-15000=1.5ms等
– 这里有一个正则表达式,可以批量替换你的默认睡眠为高精度的:
SEARCH:Sleep,\s*(\d+)
REPLACE:DllCall\(“Sleep”,“UInt”,\1\)

常见问题:
Q. 为什么不直接使用DllCall(“Winmm\timeBeginPeriod”,“UInt”,1)就像文档建议的那样?
A.该命令的最低接受值仅为 1 毫秒,而 NtSetTimerResolution 支持 0.5 毫秒

Q. 由于更改定时器分辨率会影响所有程序,为什么不将其设置为正常DllCall(“Winmm\timeEndPeriod”, “UInt”, TimePeriod)就像文档建议的那样?
A. 何必呢,退出更改定时器分辨率的程序会自动将其设置回正常值。但是,如果你想这样做,你仍然可以运行DllCall(“ntdll\ZwSetTimerResolution”,“UInt”,5000,“UInt”,0,“UInt*”,MyCurrentTimerResolution)

Q. 如果你 ZwDelayExecution 更少,会发生什么……比如说,-1000(0.1ms)?
A. 您仍将以 -5000 (0.5ms) 的增量休眠,但在我循环 ZwDelayExecution 100 次的基准测试中,由于某种原因,使用 -5000 不如使用 -1000 或 -1(在 Win10 上)

Q . 为什么使用 Zw 而不是 Nt 调用?
A. 因为如果你处于用户模式,请使用任何你认为让你的代码看起来漂亮的变体。在内核模式下,使用 ZwXxx 例程并将以前的模式正确设置为内核模式。

  • 使用时像素搜索若要扫描单个颜色变体的单个像素,请不要使用 Fast 参数。根据我的基准测试,在这种情况下,常规 PixelSearch 比 PixelSearch Fast 更快。
  • 根据文档,AHK 的 Unicode x64 位版本更快,可用时使用它(此文本可在安装文件中找到)。

(二)杂项优化

定义变量(基准)jNizM 写道:速度测试(基准测试):定义变量

#NoEnv
SetBatchLines -1

; =========================================================================================================

QPC(1)

loop 1000000
{
    t1a := 1
    t1b := 1
    t1c := 1
    t1d := 1
    t1e := 1
    t1f := 1
    t1g := 1
    t1h := 1
    t1i := 1
    t1j := 1
}
test1 := QPC(0), QPC(1)

loop 1000000
    t2a := t2b := t2c := t2d := t2e := t2f := t2g := t2h := t2i := t2j := 1
test2 := QPC(0), QPC(1)

loop 1000000
    t3a := 1, t3b := 1, t3c := 1, t3d := 1, t3e := 1, t3f := 1, t3g := 1, t3h := 1, t3i := 1, t3j := 1
test3 := QPC(0)

MsgBox % test1 "`n" test2 "`n" test3
ExitApp

; =========================================================================================================

QPC(R := 0)
{
    static P := 0, F := 0, Q := DllCall("QueryPerformanceFrequency", "Int64P", F)
    return ! DllCall("QueryPerformanceCounter", "Int64P", Q) + (R ? (P := Q) / F : (Q - P) / F) 
}

性能:在 v1.0.48+ 中,逗号运算符通常比编写单独的表达式更快,尤其是在将一个变量分配给另一个变量时(例如 x:=y, a:=b)。随着越来越多的表达式组合成一个表达式,性能不断提高;例如,将 5 个或 10 个简单表达式合并为一个表达式的速度可能会快 35%。

a := 1
b := 2
c := 3
d := Function( param )
e := AnotherFunc( d )
return
a := 1
, b := 2
, c := 3
, d := Function( param )
, e := AnotherFunc( d )
return

我的项目相当大,在我的整个脚本中这样做导致了速度的显着提高。

VarSetCapacity 优化Sam_写道:根据我的经验(并根据文档),使用

VarSetCapacity() 通过渐进式连接提高了构建字符串时的性能。从文档中:

除了 DllCall 中描述的用途外,此函数还可用于通过逐步串联来增强生成字符串时的性能。这是因为当您对字符串的最终长度有所了解时,可以避免多次自动调整大小。从文档中的示例(虽然不是一个工作示例…):

; Optimize by ensuring MyVar has plenty of space to work with.
VarSetCapacity(MyVar, 10240000)  ; ~10 MB
Loop
{
    ...
    MyVar = %MyVar%%StringToConcatenate%
    ...
}

加速 DllCalljNizM 写道:Speedup DllCall(不包括:“User32.dll”、“Kernel32.dll”、“ComCtl32.dll”和“Gdi32.dll”)

在旧论坛中找到(Bentschi 的代码)

函数:LoadLibrary() 和 FreeLibrary()

LoadLibrary(filename)
{
    static ref := {}
    if (!(ptr := p := DllCall("LoadLibrary", "str", filename, "ptr")))
        return 0
    ref[ptr,"count"] := (ref[ptr]) ? ref[ptr,"count"]+1 : 1
    p += NumGet(p+0, 0x3c, "int")+24
    o := {_ptr:ptr, __delete:func("FreeLibrary"), _ref:ref[ptr]}
    if (NumGet(p+0, (A_PtrSize=4) ? 92 : 108, "uint")<1 || (ts := NumGet(p+0, (A_PtrSize=4) ? 96 : 112, "uint")+ptr)=ptr || (te := NumGet(p+0, (A_PtrSize=4) ? 100 : 116, "uint")+ts)=ts)
        return o
    n := ptr+NumGet(ts+0, 32, "uint")
    loop % NumGet(ts+0, 24, "uint")
    {
        if (p := NumGet(n+0, (A_Index-1)*4, "uint"))
        {
            o[f := StrGet(ptr+p, "cp0")] := DllCall("GetProcAddress", "ptr", ptr, "astr", f, "ptr")
            if (Substr(f, 0)==((A_IsUnicode) ? "W" : "A"))
                o[Substr(f, 1, -1)] := o[f]
        }
    }
    return o
}

FreeLibrary(lib)
{
    if (lib._ref.count>=1)
        lib._ref.count -= 1
    if (lib._ref.count<1)
        DllCall("FreeLibrary", "ptr", lib._ptr)
}

测试脚本:

loops := 1000000
SetBatchLines, -1
global gdiplus := LoadLibrary("gdiplus")
VarSetCapacity(bin, 20, 0)
NumPut(1, bin, 0, "int")
DllCall(gdiplus.GdiplusStartup, "ptr*", token, "ptr", &bin, "ptr", 0)
DllCall(gdiplus.GdipCreateBitmapFromScan0, "int", 1, "int", 1, "int", 0, "int", 0x26200A, "ptr", 0, "ptr*", pBitmap)

start := A_TickCount
loop % loops
    DllCall("gdiplus\GdipBitmapGetPixel", "ptr", pBitmap, "int", 1, "int", 1, "uint*", col)
timeA := A_TickCount-start

start := A_TickCount
loop % loops
    DllCall(gdiplus.GdipBitmapGetPixel, "ptr", pBitmap, "int", 1, "int", 1, "uint*", col)
timeB := A_TickCount-start

DllCall(gdiplus.DisposeImage, "ptr", pBitmap)
DllCall(gdiplus.Cleanup, "ptr", token)
MsgBox % "Normal:`n" timeA "`n`nWith LoadLibrary:`n" timeB "`n`n" timeA/timeB
---------------------------
LoadLibrary.ahk
---------------------------
Normal:
1828

With LoadLibrary:
781

2.340589
---------------------------
OK   
---------------------------

导出表:

global gdiplus := LoadLibrary("gdiplus")
for name, proc in gdiplus
{
    if (name!="_ptr" && name!="_ref" && name!="__delete")
        MsgBox % name
}

三元与 if/else

Ternarry:        2.828439
if/else:         3.931492

法典:

不要将数学/变量分散在多行上SvenBent 写道:避免将数学分布在多行上,如果变量只使用一次,则避免将变量用于中间结果。
尽可能将数学浓缩为一行,并且仅在以后需要多次使用结果时才使用变量来存储数学结果。

请记住:
1 倍计算 < 1 倍计算 + 1 倍内存读取 < 2 倍计算

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.

#Singleinstance force
SetBatchlines, -1
ListLines Off
#KeyHistory 0

var_Input1:=123
var_Input2:=456


start:=A_tickcount
Loop 99999999
	{
	X:= (2 * var_Input1 ) -1
	Y:= (3 / var_Input2 ) +7
	Z:= X / Y
	}
Results1:=A_tickcount - start


start:=A_tickcount
Loop 99999999
	{
	Z:= ((2 * var_Input1 ) -1) / ((3 / var_Input2 ) +7)
	}
Results2:= A_tickcount - start


msgbox %Results1% - %Results2%

第二种浓缩方法的速度略高于 50%

Gosub VS 函数SvenBent 写道:这可能是微不足道的,但
使用 Gosub 似乎比使用函数快 25%。

SetProcessWorkingSetSize VS EmptyWorkingSetSvenBent 写道:另一个微小的速度提示

:更换

    DllCall("SetProcessWorkingSetSize", Int,-1, Int,-1, Int,-1 )

    return, dllcall("psapi.dll\EmptyWorkingSet", "UInt", -1)

在空内存调用中,您可以获得大约 27% 的加速

使用 if 检查布尔值SvenBent 写道:布尔值
IF 检查的一些技巧 在 Core2Quad Q6600 系统上测试。

if VariableName
似乎是检查变量是否为 True

的最快方法 if VariableName = 0
是检查变量是否为 false 的最快方法,但它不考虑未设置的变量,即空。如果变量未设置/为空

,则 IF 命令不会被激活,如果 VariableName <> 1
几乎一样快,并且空变量被认为是假的(又名 IF 设置被激活),就像如果它包含 0

if Not VariableName
似乎比上述两个都慢

二、 AutoHotkey v2的优化技巧

在过去的几个月里,我一直在微调我在 AHKv2 中的技能,并遇到了一些关于优化的有趣发现。在探索过程中,我意识到在 AHKv1 中有效的一些速度优化技巧并不能很好地转换为 AHKv2。

关键点:在 AHKv1 中,使用逗号来链接命令和组合行是加快脚本速度的常见做法。但是,在 AHKv2 中,这些方法会导致执行速度变慢,以下是一些推测:

使用逗号的命令链接:在 v1 中,此技术减少了行数,并可能加快执行速度。然而,在 v2 中,解释器的改进意味着通过链接命令节省的开销不再存在。我发现在链条时实际上有一个速度阻尼器。请务必注意,这些观察结果也有例外,在某些特定用例中,您可能仍会在 v2 中看到性能优势。

我见过一些最有经验的 v2 编写者使用的一些旧的优化方法,所以我认为我编写了错误的 DLL 调用。您可以在前面提到的线程中找到 v1 测试,并在此处找到 v2 测试。我鼓励你复制这些测试并分享你的发现。我已经坐了大约 3 个月,并发布到 subreddit 进行审查。任何可复制的计数器都将共享。

我使用 DLLCall 查询计数器进行了操作,每个查询计数器循环运行 55,000 次,检查并重新检查了我的数字,并提供了可以复制冗余的地方。也就是说,让我们一起继续学习和改进。

我为此任务编写了一个特定于帮助程序 GUI 的测试

,可以在此处找到: https://github.com/samfisherirl/Compare-Code-Speed-GUI-AHKv2
在调试模式下运行类似的脚本会提供不同的输出。
– 它通过调用 QueryPerformanceFrequency,
– 执行次数的循环选项
– 循环总数一分为二(并四舍五入),并在蛇形迭代中执行。
例如,如果用户有 3 个用于测试的脚本,并且测试运行了 1000 次,则脚本将执行:

script1.test1() x500
script2.test1() x500
script3.test1() x500
script3.test2() x500
script2.test2() x500
script1.test2() x500
s1.t1+s1.t2/2

这个想法是希望任何内存、CPU 或其他速度减慢,如用户设置全局附加变量。脚本写入临时文件,并通过 Run A_AHKDir\ahk.exe [–] scriptpath 执行,脚本仅在完成迭代后写入日志文件。

我不会尝试重写原始的帖子文本,其中很多都很棒,应该从源头阅读.许多公约,如 #NoEnv 和 #SetBatchLines,已经不复存在,但其余的非常有价值。我已经重组了 ahkv2 的测试。

如果您想在 v1 和 v2 中复制测试,请使用上面的链接和 v2 中的这组转换: https://github.com/samfisherirl/Compare-Code-Speed-GUI-AHKv2/tree/main/_speedTestScripts

在一行上组合表达式,为简洁起见,逗号
测试缩短,请访问上面的链接以获取完整的脚本

;test 1
t1a := 1
t1b := 1
t1c := 1
t1d := 1 
; this continues for a few more lines but I will be abbreviating all duplicates 

;test 2
 t2f := t2g := t2h := t2i := t2j := 1

;test3
t3a := 1, t3b := 1, t3c := 1, t3d := 1

这些结果仅在每个版本的 AHK 中进行比较。它们不是 ahkv1:ahkv2 的 1:1,也不是为了展示 ahkv1 和 ahkv2 之间的速度差异,而是展示各自版本中测试之间的差异。单个版本中的所有测试都使用一致的版本和环境运行。

AHKv1 结果 =

test1 0.240315
test2 0.132753 ;重复 50% 更快
test3 0.168953 ;与多行

AHKV2 结果相比,使用逗号的速度提高了 ~35% =

– test1 0.00124844 ;50% + 更快 将变量放在单独的行
上 – test2 0.00259254
– test3 0.00274485

我们可以看到,在这些示例中,将变量组合在一行上不再更快,而是妨碍了代码。我们会发现这与函数调用不同。

让我们用函数再做一次

	
	; these functions are across all and each test in this trio  ; condensed

    e() {   y := 999*222
	   return y }

	f() {  y := 999*222
	   return y }
	g() {   y := 999*222
	   return y }
	   
	
; test1 (each function written above)
a := e()
b := f()
c := g()
	
; test2 
a := e(),b := f(),c := g()

;test3
a := e()
,b := f()
,c := g()

AHKV2 结果

– test1 0.01627(慢 50%)
– test2 0.01098
– test3 0.011008

即使是缩短的条件句,使用组合行也不会更快

;test1 
x := true

if x
   z:=1, a:=2, b:=1, c:=2
   
;test2 
x := true
if x
{ 
	   z:=1
	   a:=2
	   b:=1
	   c:=2
}

AHKv2 结果=
test1 0.0026
test2 0.00180 ;速度提高 30%

三、其他通用优化技巧

除了针对特定版本的优化技巧外,还有一些通用的优化方法可以在AutoHotkey脚本中使用,无论是v1还是v2版本都适用:

(一)合理使用函数

将代码模块化为函数可以提高代码的可维护性和可重用性。在需要重复执行的代码段中,尽量将其封装成函数,并根据需要进行调用。

; 定义函数
MyFunction() {
    ; 函数体
}

; 调用函数
MyFunction()

(二)避免不必要的循环和条件判断

在编写循环和条件语句时,要尽量避免不必要的操作和判断,以减少脚本的执行时间。优化循环结构和条件判断逻辑可以显著提升脚本的性能。

Loop, 1000 {
    ; 循环体
}

(三)减少对外部资源的依赖

尽量减少脚本对外部资源(如文件、网络等)的依赖,以降低脚本的执行时间和提高脚本的稳定性。合理管理资源的加载和释放,可以有效地提高脚本的性能。

; 加载外部资源
FileRead, Content, C:\data.txt

; 处理资源数据
MsgBox % Content

(四)使用合适的数据结构

根据脚本的需求和特性,选择合适的数据结构可以提高脚本的效率。例如,使用数组或对象来管理数据集合,使用哈希表或树来进行高效的数据查找和操作。

; 定义数组
MyArray := ["A", "B", "C"]

; 遍历数组
Loop, % MyArray.MaxIndex() {
    MsgBox % MyArray[A_Index]
}

四、结语

通过综合应用以上优化技巧,您可以显著提高AutoHotkey脚本的执行速度,从而提升用户体验并实现更复杂的功能。在编写脚本时,建议根据具体需求和场景选择合适的优化方法,并定期对脚本进行性能评估和调整,以确保脚本的高效运行。

优化AutoHotkey脚本的速度是一个持续改进的过程,希望本指南能够为您提供有价值的参考和指导,让您的脚本在实际应用中表现更加出色。

给TA捐赠
共{{data.count}}人
人已捐赠
教程

利用程序设计原则打造 AutoHotkey 脚本:编写自动化利器的秘籍!

2024-3-20 18:26:09

教程

AutoHotkey管理(关闭)Windows进程

2024-3-25 21:48:14

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
有新私信 私信列表
搜索