1

Тема: AHK: Ускорение скриптов машинным кодом

Идея взята из темы Machine code functions: Bit Wizardry. Суть в том, что начиная с версии 1.0.46.08 в DllCall можно вместо имени функции указывать её адрес. Таким образом, если вырезать из какого-либо EXE- или DLL-файла код какой-либо функции и поместить в переменную в скрипте, то эту функцию можно потом вызывать через DllCall и она будет прекрасно работать — при условии, что в ней отсутствуют внешние ссылки, т.е. вызовы каких-то других функций или обращения к внешним данным. Функция должна взаимодействовать с внешним миром только через свои аргументы и возвращаемое значение.

Эффект от применения машинного кода может быть очень велик в случаях, когда нужно обрабатывать большой объём данных. Для примера можно взять задачу, обсуждавшуюся на форуме: AHK: вычисление простых чисел. При поиске в диапазоне до миллиона скрипто-машинный код, приведённый ниже, быстрее чисто скриптового в 160 раз, дальше разница ещё больше. Машинная функция написана мной на ассемблере и скомпилирована при помощи The Go tools for Windows + Assembler.

#MaxMem, 256    ; Нужно, например, если Limit = 100.000.000

Limit = 1000000    ; Проверять от 1 до этого числа включительно.
OutFile = %A_Desktop%\MyPrimes.txt

IfExist, %OutFile%
    FileDelete, %OutFile%

        time0 := A_TickCount    ; Замер времени, в научных целях.

; Вызов скриптовой функции - оболочки машинного кода (см. ниже).
; Возвращает количество найденных простых чисел.
Found := PrimeErat(Buf, Limit, 1) ; 1 - конвертировать в строку, 0 - нет.

        time1 := A_TickCount - time0

FileAppend, %Buf%, %OutFile%  ; Так сохранится, только если в виде строки.

        time2 := A_TickCount - time0 - time1
        time := A_TickCount - time0

; Точки между тройками цифр в MsgBox ниже — для разборчивости.
strLimit := RegExReplace(Limit, "(?<=\d)(\d\d\d)(?=\d\d\d$|$)", ".$1")
strFound := RegExReplace(Found, "(?<=\d)(\d\d\d)(?=\d\d\d$|$)", ".$1")

MsgBox, 
(
Проверено: %strLimit%
Найдено: %strFound%

Поиск и в строку: %time1% мс
В файл: %time2% мс
Общее время: %time% мс

Сохранено:
%OutFile%
)

; ================= Функция поиска ===================

PrimeErat(ByRef Buf, Limit, ToString)
{
    Hex =        ; Собственно машинный код, в HEX-виде.
    (Join LTrim 
    5553575689E583EC0C8B5D148B4D1831C083F9020F8CCE0000008D344B89D983
    7D1C00740E0FBAEE0066C701322083C102EB09C7010200000083C104894DF489
    DFC745FC01000000FC83C70439F70F87850000008B0785C078EFFF45FC89F829
    D8D1E8400FBAE600732F505357568B7DF4BB0A00000031C989E64EC606204131
    D2F7F380C2304E88164185C075F1F3A4897DF45E5F5B58EB0B8B4DF4890183C1
    04894DF489C1F7E0729F3B4518779A48D1E001D8BAFFFFFFFF891089C789C8C1
    E00201C739F777048917EBF6D1E883E8028D3C03E970FFFFFF8B45FC0FBAE600
    73068B55F4C6020089EC5E5F5B5DC20C00
    )
    VarSetCapacity(Code, Len := (StrLen(Hex) // 2)) ; Переменная под код.
    Char := Offset := -1
    Loop, % Len            ; Запись кода функции в переменную.
        NumPut("0x" . SubStr(Hex, Char += 2, 2), Code, ++Offset, "Char")
    VarSetCapacity(Buf, (Limit+1) * 2, 0)    ; Под найденные простые числа.
    ; Вызов машинной функции. Вернёт количество найденных простых.
    ; Первый аргумент DllCall — адрес переменной, куда был записан код.
    Found := DllCall(&Code, "uint", &Buf, "uint", Limit, "int", ToString)
    ; Если была конверсия в строку, нужно обновить переменную, иначе
    ; она нормально не отобразится и не запишется в файл.
    If ToString
        VarSetCapacity(Buf, -1)

    Return Found
}

Исходник машинного кода:

CODE SECTION

Start FRAME pArray, uLimit, iStr
      USES ebx,edi,esi
      LOCALS uCount, uNum, pResCur

    mov ebx,[pArray]
    mov ecx,[uLimit]
    xor eax,eax        ; Found = 0, если uLimit < 2.
    cmp ecx,2
    jl >>.exit
        lea esi,[ebx+ecx*2]
        mov ecx,ebx
        cmp D[iStr],0
        je >
            bts esi,0
            mov W[ecx],2032h    ; 2 и пробел.
            add ecx,2
            jmp > 1
        :
        mov D[ecx],2
        add ecx,4
            1:
        mov [pResCur],ecx
        mov edi,ebx
        mov D[uCount],1
        cld
.find_next
    add edi,4
    cmp edi,esi
    ja >>.done
        mov eax,[edi]
        test eax,eax
        js .find_next
            inc D[uCount]
            mov eax,edi
            sub eax,ebx
            shr eax,1
            inc eax
            bt esi,0
            jnc >
                push eax,ebx,edi,esi
                mov edi,[pResCur]
                mov ebx,10
                xor ecx,ecx
                mov esi,esp
                dec esi
                mov B[esi],' '
                inc ecx
                1:
                    xor edx,edx
                    div ebx
                    add dl,30h
                    dec esi
                    mov [esi],dl
                    inc ecx
                    test eax,eax
                jnz < 1
                rep movsb
                mov [pResCur],edi
                pop esi,edi,ebx,eax
                jmp > 1
            :
            mov ecx,[pResCur]
            mov [ecx],eax
            add ecx,4
            mov [pResCur],ecx
                1:
            mov ecx,eax
            mul eax
            jc .find_next
                cmp eax,[uLimit]
                ja .find_next
                    dec eax
                    shl eax,1
                    add eax,ebx
                    mov edx,-1
                    mov [eax],edx
                    mov edi,eax
                    mov eax,ecx
                    shl eax,2
.mark_out
    add edi,eax
    cmp edi,esi
    ja >
        mov [edi],edx
        jmp .mark_out
    :
    shr eax,1
    sub eax,2
    lea edi,[ebx+eax]
    jmp .find_next
.done
    mov eax,[uCount]
    bt esi,0
    jnc >.exit
        mov edx,[pResCur]
        mov B[edx],0
.exit
    ret

ENDF