Tutorial Optimizing

From AutoIt Wiki
Revision as of 01:26, 4 June 2014 by Poila (talk | contribs) (→‎The more accurate approach: Minor grammar edits)
Jump to navigation Jump to search

NOTE: This document is work in progress. Your view and contribution on content is welcome. Spellchecking and grammar will be worked on when the document approaches a stable state.

Introduction

This tutorial will be about profiling and optimizing code when we have identified a bottleneck. There are several paths down this road. You could analytically evaluate your code, or fragments of it. You could optimize parts that don't feel good. And you can measure how long time the code takes to run.

Profiling

When we profile the code we use a stopwatch to log how much time that is spent from the code starts until it ends execution. There are two types of time we can use. The real time spent and the CPU time spent to execute the code. Both methods have its usage. But measuring the CPU time should normally result in better knowledge than measuring clock time. The exception could be in routines where the code calls external resources that depends on other factors rather than the CPU. Ex: printer, network, disk IO.

The simplest approach

Autoit has two handy clock functions to do a regular stopwatch measure of code execution time. Namely TimerInit and TimerDiff. TimerInit is used to get a reference point and TimerDiff to get the difference between the reference point and the call to TimerDiff in milliseconds. So the simplest possible sample could be:

 Local $i, $j, $sum, $count, $ts, $td
 For $j = 1 to 10
	$ts = TimerInit()
 	Do
 		$count += 1
 		$sum += Random(-5, 5, 1)
 	Until $sum = 0 OR $count = 100000
 	; Write/create report
 	$td = TimerDiff($ts)
 	ConsoleWrite( StringFormat("%#.6g ms, Iter=%.6d, avg=%.6g", $td, $count, $td/$count) & @LF)
 	$count = 0
 Next

Now although this is a reasonable way of measuring execution time, it does not give an accurate picture to identify bottlenecks. And the reason is that it measures real time and not the time spent by the CPU to execute this part of the code. You can probably see this if one of the iterations reaches a $sum=0 in few iterations compared to one that uses max iterations.

The more accurate approach

API notes: GetProcessTimes, GetThreadTimes Windows versions based on NT kernel has at least two API calls to make profiling easier for us. GetProcessTimes will return the CPU time spent by the given process. GetThreadTimes will return the CPU time spent by the given thread. As AutoIt does not support user defined threads, we will use the GetProcessTimes call. A profiling UDF has been created by @uten in the AutoItScript forum. As for now we will use that code. Save the code in an include file. I have called mine profiler.au3 and saved it in an include directory. (TODO: Link to page explaining userdefined include directories.)

Global $FILETIME = "dword;dword"    ;Contains a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601 (UTC).
Global $SYSTEMTIME = "ushort;ushort;ushort;ushort;ushort;ushort;ushort;ushort"
Func FileTimeToNum(ByRef $FT)
   Return BITOr(BitShift(DllStructGetData($FT, 1), 32), DllStructGetData($FT, 2))
EndFunc
Func OpenProcess($pid = @AutoItPID)
    Local $PROCESS_QUERY_INFORMATION = 0x0400
    Local $PROCESS_VM_READ = 0x0010
    $Process = DLLCall("kernel32.dll","int","OpenProcess","int", _
            BitOR($PROCESS_QUERY_INFORMATION,$PROCESS_VM_READ),"int",0,"int", $pid)
    If $Process[0] = 0 Then Return SetError(1, 0, 0)
   Return $Process[0]
EndFunc
Func GetProcessTimes(ByRef $hProcess, ByRef $t1, ByRef $t2, ByRef $t3, ByRef $t4)

;BOOL WINAPI GetProcessTimes(
;  HANDLE hProcess,
;  LPFILETIME lpCreationTime,
;  LPFILETIME lpExitTime,
;  LPFILETIME lpKernelTime,
;  LPFILETIME lpUserTime
; )
;
   Local $p1 = DllStructGetPtr($t1)
   Local $p2 = DllStructGetPtr($t2)
   Local $p3 = DllStructGetPtr($t3)
   Local $p4 = DllStructGetPtr($t4)
   ;ConsoleWrite($p1 & ", " & $p2& ", " & $p3 & ", " & $p4 & @LF)
   Local $ret = dllcall("kernel32.dll","int", "GetProcessTimes","int",$hProcess ,  "ptr", $p1, "ptr", $p2, "ptr", $p3, "ptr", $p4)
   If $ret[0] = 0 Then
      ConsoleWrite("(" & @ScriptLineNumber & ") : = Error in GetProcessTimes call" & @LF)
      SetError(1, 0 , $ret[0])
   EndIf
   Return $ret[0]
EndFunc
Func ProfileInit()
   Local $process = OpenProcess(@AutoItPID)
   if @error then
      ConsoleWrite("!OpenProcess failed terminating" & @LF)
      Exit
   EndIf
   Local $ret[4]
   $ret[0] = 2
   Local $t1 = DllStructCreate($FILETIME)
   Local $t2 = DllStructCreate($FILETIME)
   Local $t3 = DllStructCreate($FILETIME)
   Local $t4 = DllStructCreate($FILETIME)    
   If GetProcessTimes($process, $t1, $t2, $t3, $t4) Then
      $ret[$ret[0] + 1] = $process
      $ret[1] = FileTimeToNum($t3)
      $ret[2] = FileTimeToNum($t4)
   Else
      ConsoleWrite("(" & @ScriptLineNumber & ") := @error:=" & @error & ", @extended:?" & @extended & @LF)
      SetError(@error, @extended, 0)
   EndIf
   ;ArrayDump($ret, "ProfileInit Out")
   Return $ret
EndFunc
Func ProfileDiff(ByRef $init)
   Local $ret[3]
   $ret[0] = 2
   ;ArrayDump($init, "ProfileDiff($init)")
   Local $t1 = DllStructCreate($FILETIME)
   Local $t2 = DllStructCreate($FILETIME)
   Local $t3 = DllStructCreate($FILETIME)
   Local $t4 = DllStructCreate($FILETIME)    
   Local $n1, $n2
   If GetProcessTimes($init[$init[0] + 1], $t1, $t2, $t3, $t4) Then
      $ret[1] = FileTimeToNum($t3) - $init[1]
      $ret[2] = FileTimeToNum($t4) - $init[2]
   Else
      ConsoleWrite("(" & @ScriptLineNumber & ") := @error:=" & @error & ", @extended:=" & @extended & @LF)
      SetError(@error, 0 , 0)
   EndIf
   Return $ret
EndFunc

Now that we have a some profiling routines. ProfileInit and ProfileDiff we can change our sample to make use of those UDF's. Note that the time returned from ProfileDiff are in 100-nanoseconds intervals. To get ms we have to divide by 10000.

 
 #include <profiler.au3>
 Local $i, $j, $sum, $count, $ps, $pd, $uk
 For $j = 1 to 10
	$ps = ProfileInit()  
 	Do
 		$count += 1
 		$sum += Random(-5, 5, 1)
 	Until $sum = 0 OR $count = 100000
 	; Write/create report
 	$pd = ProfileDiff($ps)
 	$uk = Round(($pd[1] + $pd[2])/10000, 0) ;User and kernel time
 	ConsoleWrite( StringFormat("%#.6g ms, Iter=%.6d, avg=%.6g", $uk, $count, $uk/$count) & @LF)
 	$count = 0
 Next

Optimizing code

When do we need to optimize our code? Well, thats a good question. A roule of thumb is to make the code work first. If we experience that the code is slow we have to find the reason. When one or more reasons are found we have to consider if it is possible to optimize the code.

Let's have a look at a real world sample. Sorting stuff is a classic.

Getting some data

Let's grab some data from autoitscript. The data is not relevant for the sample so if you have a fearly big html file on your disk you could use that instead.

#include <inet.au3>
$data = _InetGet("http://www.autoitscript.com/forum/index.php?showtopic=36941")
;$data = ReadFile("<your file>")

Data to plain text. Take one

Func WordList(ByRef $data)
;Shave of html tags
Local $res
;Return string
EndFunc

Data to plain text. Take two

Func Wordlist2(ByRef $data)
Local $res = StringRegExp($data, "TODO", 3)
;Array to string
;Return string
EndFunc

Sorting the text. Take one

Using bouble sort

Sorting the text. Take two

Using something faster.