Anyway, this example is not really finished and I will surely advance it further,
but you will get the idea
So, I wanted an easy method of using all my CPU cores to do complex calculations.
I didn't really test it yet, but if I didn't really do a great mistake (possible,
cause I'm in a hurry right now ), it really is twice as fast now.
Ok, let's see:
1. Example, use the standard way. 2 For-Loops to calculate an array of values.
I do it for 2 seconds and count how many times it calculated:
Code: Select all
DisableDebugger
;- EXAMPLE
maxX = 1400
maxY = 1050
Global Dim a.d(maxX, maxY)
Delay(100)
time = ElapsedMilliseconds()
counter = 0
Repeat
For y = 0 To maxY
For x = 0 To maxX
c.l = (Int(y) ! Int(x)) & $FF
a(x, y) = Pow(c, x)
Next
Next
counter + 1
Until ElapsedMilliseconds() - time > 2000
MessageRequester("Fertig", "Durchläufe: "+Str(counter))
Ok, now let's try the ParallelFor:
Code: Select all
DisableDebugger
EnableExplicit
CompilerIf #PB_Compiler_Thread
CompilerElse
CompilerError "In order to use ParallelFor, you have to compile as thread safe."
CompilerEndIf
; a procedure to get the number of CPUs/Cores
; somebody could add the windows version ;)
Procedure.l GetCPUCount()
CompilerIf #PB_Compiler_OS = #PB_OS_Linux
Protected file.l, count.l, s.s, dir.l
If FileSize("/proc/cpuinfo") <> -1
file = ReadFile(#PB_Any, "/proc/cpuinfo")
If IsFile(file)
count = 0
While Not Eof(file)
s.s = ReadString(file)
If Left(s, 9) = "processor"
count + 1
EndIf
Wend
CloseFile(file)
EndIf
Else
dir = ExamineDirectory(#PB_Any, "/proc/acpi/processor", "")
count = 0
If IsDirectory(dir)
While NextDirectoryEntry(dir)
If Left(DirectoryEntryName(dir), 3) = "CPU"
count + 1
EndIf
Wend
FinishDirectory(dir)
EndIf
EndIf
ProcedureReturn count
CompilerElse
ProcedureReturn 2
CompilerEndIf
EndProcedure
; this is the callback procedure lookalike. If the callback returns #True,
; the thread will terminate.
; i.l = the variable that will be incremented like in "For i = 80 to 110"
; *dataPtr = a value you can specify that will be given to each callback
Prototype.l ParallelLoopFunc(i.l, *dataPtr)
; internal structures
Structure PARALLELLOOP
i1.l ; starting number
i2.l ; finishing number
loop.ParallelLoopFunc ; callback prototype
*dataPtr ; a value given to the callback
EndStructure
Structure PARALLELTHREAD ; a structure for ParallelForLinear()
id.l ; ThreadID
info.PARALLELLOOP ; Thread-Data for running the callback
EndStructure
; This is the actual thread:
Procedure _ParallelFor_Thread(*info.PARALLELLOOP)
Protected i.l
; in *info, all the information is stored, like
; start and stop value for 'i'
; or the data to be given to the callback
With *info
; now run the callback with 'i' as the first
; parameter exactly (\i1 - \i2 + 1) times
For i = \i1 To \i2
; call the Callback with 'i' and '*dataPtr'
; and check, if it returns #True.
If \loop(i, \dataPtr)
; if returned #true, break and therefore
; terminate the thread
Break
EndIf
Next
EndWith
; I have finished my job -> terminate thread
EndProcedure
; ParallelFor() is a procedure that will create 2 threads:
; i1 = thread1, start value
; i2 = thread1, stop value
; j1 = thread2, start value
; j2 = thread2, stop value
; loopFunc = pointer to the callback function
; *dataPtr = a value given to each callback function
Procedure ParallelFor(i1.l, i2.l, j1.l, j2.l, loopFunc.ParallelLoopFunc, *dataPtr)
Protected *th1, *th2
Protected info1.PARALLELLOOP, info2.PARALLELLOOP
; these requirements have to be fullfilled
If loopFunc And i1 <= i2 And j1 <= j2
; create the sub-job for thread 1:
With info1
\i1 = i1 ; start
\i2 = i2 ; stop
\loop = loopFunc
\dataPtr = *dataPtr
EndWith
*th1 = CreateThread(@_ParallelFor_Thread(), @info1)
If *th1
; if thread 1 has been created, create thread 2
With info2
\i1 = j1
\i2 = j2
\loop = loopFunc
\dataPtr = *dataPtr
EndWith
*th2 = CreateThread(@_ParallelFor_Thread(), @info2)
; and now wait for both threads to terminate
If *th2
WaitThread(*th1)
WaitThread(*th2)
Else
WaitThread(*th1)
EndIf
EndIf
EndIf
EndProcedure
; ParallelForLinear() is a procedure that will create 1 or more threads
; and split the job into equal sub-jobs/threads
; i1 = start value
; i2 = stop value - be sure that i2 is significantly larger than i1
; loopFunc = pointer to the callback function
; *dataPtr = a value given to each callback function
; cores = there you can specify, how many threads shall be generated. You can
; use GetCPUCount() for this to use all cores.
Procedure ParallelForLinear(i1.l, i2.l, loopFunc.ParallelLoopFunc, *dataPtr, cores.l = 2)
Protected NewList threads.PARALLELTHREAD()
Protected z.l, stepSize.l, j.l, j2.l
If loopFunc And i1 <= i2 And cores >= 1
; stepSize is the range every thread has to process
stepSize = (i2 - i1) / cores
j = i1 ; first thread starts with i1
; for each core, create a job
For z = 1 To cores
; the 'stop' for the thread will be start+stepSize:
j2 = j + stepSize
If j2 > i2
j2 = i2
z = cores ; stop the For-Loop if no things for other cores
EndIf
If j > i2
; just to be on the safe side. not tested that much
Break
EndIf
; create a new thread/sub-job
AddElement(threads())
With threads()\info
\i1 = j
\i2 = j2
\loop = loopFunc
\dataPtr = *dataPtr
EndWith
j + stepSize + 1 ; next thread will continue with the work
Next
; now we will start all the threads
; it's now because I don't want to know what can happen if
; I access the LinkedList within the threads and still adding
; new elements
ForEach threads()
threads()\id = CreateThread(@_ParallelFor_Thread(), @threads()\info)
If threads()\id = 0
; if something went wrong, break up
Break
EndIf
Next
; wait for each thread to stop
ForEach threads()
WaitThread(threads()\id)
Next
EndIf
EndProcedure
;- HOWTO:
; In general:
; If you have a BIG For-Loop (BIG means either many many iterations, big
; loop-body or in general, looong execution times (so that the overhead
; produced by starting threads etc. doesn't matter)), then you just have
; to follow these few steps:
; - Be sure, that no iteration of the loop depends on another iteration
; i.e. you cannot use the result of a calculation that was done an
; iteration earlier.
; - Be sure, that you are able to synchronize the loop-body so that
; threads can safely do their work -> Thread safety!
; - Then just replace a for-loop like
; For x = 50 to 50000
; with
; ParallelForLinear(50, 50000, @LoopBody(), @myData, GetCPUCount())
; - Capsule the loop body into a LoopBody() procedure that follows the
; definition of the prototype ParallelLoopFunc
; and you should be done.
;- EXAMPLE
DisableExplicit
maxX = 1400
maxY = 1050
Global Dim a.d(maxX, maxY)
Delay(100)
time = ElapsedMilliseconds()
counter = 0
Procedure InnerLoop(i.l, maxX.l)
For x = 0 To maxX
c.l = (Int(y) ! Int(x)) & $FF
a(x, y) = Pow(c, x)
Next
EndProcedure
Repeat
ParallelForLinear(0, maxY, @InnerLoop(), maxX, GetCPUCount())
; For y = 0 To maxY
; For x = 0 To maxX
; c.l = (Int(y) ! Int(x)) & $FF
; a(x, y) = Pow(c, x)
; Next
; Next
counter + 1
Until ElapsedMilliseconds() - time > 2000
MessageRequester("Fertig", "Durchläufe: "+Str(counter))
Yeah, it doesn't scale up to 4 or more cores, but let's wait'n'see
Edit: Added comments and an alpha version of the linear scaling version