Toon posts:

[VB6] Hoe een For-Next-lus vervangen door efficiëntere code?

Pagina: 1
Acties:
  • 266 views sinds 30-01-2008
  • Reageer

Verwijderd

Topicstarter
Deze is voor de abstracte denkers onder ons die goed zijn in het optimaliseren van code in VB6. Dat is niet mijn sterkste punt en dat brengt me momenteel in de problemen. Hopelijk kan ik hier iets leren van de suggesties van anderen.

Ik heb (in essentie) de volgende code:
code:
1
2
3
4
5
6
7
8
9
    Dim bytBuf() As Byte
    ReDim bytBuf(0 To param1) As Byte
    Dim z As Long
        
        ReadFile user, bytBuf(0), param1, lngRead, ByVal 0&
        For z = 0 To param1
            bytBuf(z) = 255 - bytBuf(z)
        Next z
        CopyMemory ByVal param2, bytBuf(0), UBound(bytBuf)

Deze code wordt om de zoveel tijd aangeroepen. Er wordt eigenlijk param1 bytes aan data uit een bestand ingelezen. Vervolgens wordt elke byte als het ware geïnverteerd (255 wordt 0, 245, wordt 10, etc) en uiteindelijk naar de variabele param2 geschreven (param2 is een pointer).

Opzich werkt deze code goed, alleen is het te langzaam. Ik zit me af te vragen hoe ik deze code kan optimaliseren. Vooral de For-Next-lus lijkt me een doorn in het oog, zeker omdat param1 waarden van 32768 kan aannemen en dan duurt zo'n lus dus wel even. Ik kan echter geen andere manier bedenken om elke byte afzonderlijk te inverteren.

Heeft iemand optimalisatie-suggesties zodat de code sneller wordt? :)

  • Tomatoman
  • Registratie: November 2000
  • Laatst online: 28-04 18:15

Tomatoman

Fulltime prutser

In VB is het vast mogelijk om te zien welke assembler code de compiler ervan maakt. Wat je dan zult zien is dat regel 6 t/m 8 in je code slechts een paar regels assembler opleveren, die in totaal in hooguit enige tientallen clock cycles worden voltooid. Laten we uitgaan van 30 clock cycles (zomaar een aanname). Als die lus 32768 keer wordt doorlopen, kost dat 30 x 32.768 ≈ 1.000.000 clock cycles. Op een pc die op 2 GHz draait komt dat overeen met een tijdsbestek van een halve milliseconde. Dat is verwaarloosbaar. Kortom: de for-next-lus optimaliseren is zonde van je tijd en de CPU-tijd wordt blijkbaar ergens anders opgesoepeerd.

Een goede grap mag vrienden kosten.


  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
tomatoman schreef op donderdag 29 september 2005 @ 01:21:
In VB is het vast mogelijk om te zien welke assembler code de compiler ervan maakt.
In VB(6) in ieder geval niet, tenzij je zelf met een disassembler aan de slag gaat. Ik vrees echter dat VB6 kennende er wel iets meer dan een "paar regels assembly" uit komen (al is het maar omdat VB6 niet erg strong-typed is en er dus constant allerlei bounds e.d. gechecked worden).

Overigens zou ik hebben gekozen voor
Visual Basic 6:
1
byteBuf(z) = Not bytBuf(z)
in plaats van 255 - .... maar goed, of dat écht iets uit haalt weet ik zo net nog niet. Je kunt natuurlijk die loop ook even timen en kijken of hier écht het probleem zit.

Bij nader inzien zit het probleem waarschijnlijk meer in het redim statement ofzo als je grote arrays gaat maken. En is param1 een integer (en dus max. 32767) of is het een long? Want anders kun je zowieso maar files tot 32Kb af...

En compile je naar P-code of native code? En staan al je bounds-checks etc. uit? (Dat kan wel eens helpen volgens mij). Tot slot zou je natuurlijk de For...next kunnen vervangen door een While of Do...Loop constructie, maar ook die zullen elkaar niet veel schelen als het uberhaupt al iets scheelt.

Timen van code:
Visual Basic 6:
1
2
3
4
5
6
7
8
9
Dim sStart as single

sStart = timer

...code hier...
...code hier...
...code hier...

Debug.print "Time taken: " & FormatNumber(Timer-sStart,5)

Helaas werkt de "Timer" met een resolutie van 0.015 sec. ofzo en dus niet erg precies op "korte stukjes". Zodra het zeg maar >0.1 sec. gaat duren kun je er aardig mee meten.


Nou, heb het even getest op wat simpele stukjes code:
Visual Basic 6:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
    Const cMax = 10000000
    
    Dim sStart As Single
    Dim bytArray(cMax) As Byte
    Dim T As Long
    
    'Array vullen met zooi
    Debug.Print "Init array: ";
    sStart = Timer
    For T = 0 To cMax
        bytArray(T) = CByte(Rnd * 255)
    Next
    Debug.Print "Time taken: " & FormatNumber(Timer - sStart, 5)

    'De "255-..." methode
    Debug.Print "Invert array (255-...): ";
    sStart = Timer
    For T = 0 To cMax
        bytArray(T) = 255 - bytArray(T)
    Next
    Debug.Print "Time taken: " & FormatNumber(Timer - sStart, 5)


    'De "Not" methode
    Debug.Print "Invert array (Not): ";
    sStart = Timer
    For T = 0 To cMax
        bytArray(T) = Not (bytArray(T))
    Next
    Debug.Print "Time taken: " & FormatNumber(Timer - sStart, 5)

    'Met een while/wend
    Debug.Print "Invert array (While/wend): ";
    sStart = Timer
    T = 0
    While T <= cMax
        bytArray(T) = Not (bytArray(T))
        T = T + 1
    Wend
    Debug.Print "Time taken: " & FormatNumber(Timer - sStart, 5)

    'En nu met een do...loop
    Debug.Print "Invert array (Do...Loop): ";
    sStart = Timer
    T = 0
    Do
        bytArray(T) = Not (bytArray(T))
        T = T + 1
    Loop Until T > cMax
    Debug.Print "Time taken: " & FormatNumber(Timer - sStart, 5)

Output:
code:
1
2
3
4
5
Init array: Time taken: 1,57813
Invert array (255-...): Time taken: 1,32813
Invert array (Not): Time taken: 1,09277
Invert array (While/wend): Time taken: 1,50000
Invert array (Do...Loop): Time taken: 1,28125

Let wel: Het gaat hier om 10000001 (0..10000000) ) elementen in de array. En dit is de output bij 32768 elementen in de array (dus 0..32767 door de const cMax op 32767 te zetten):
code:
1
2
3
4
5
Init array: Time taken: 0,00000
Invert array (255-...): Time taken: 0,00000
Invert array (Not): Time taken: 0,00000
Invert array (While/wend): Time taken: 0,00000
Invert array (Do...Loop): Time taken: 0,00000

Op een 2.8 Ghz HT Pentium 4. Er is dus wel degelijk een verschil zoals je kunt zien, en dan heb ik het nog niet eens gecompiled maar run ik het in de IDE. Je zou het om het nog accurater te krijgen moeten meten met een Hi-res timer (maar op zo'n bult elementen maakt dat natuurlijk weinig uit) en de test een X aantal maal herhalen. Maar goed, je ziet zelf al wat het "best" is.

[ Voor 118% gewijzigd door RobIII op 29-09-2005 01:53 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


Verwijderd

Topicstarter
Wauw RobIII, Super-bedankt!

Ik wist niet eens dat je met Not zo'n invert kon bewerkstelligen, dus bedankt daarvoor. :)

param1 neemt helaas echt de waarde 32768 aan en daardoor kan ik z dus niet dimensioneren als Integer maar moet het als Long. Ik had echter begrepen dat Long data types sneller zijn in VB6 dan Integers.

De code-snelheid lijkt inderdaad beter nu, met minder crashes in de IDE. De reden dat de code zo snel moet zijn is dat het in een functie staat die op een bepaald moment wordt aangeroepen via AddressOf. Als ik het goed begrijp betekent dat dat die functie eigenlijk z'n eigen thread krijgt en multi-threading is sowieso niet fijn in VB6.

Als het goed is crasht de applicatie als de functie nog loopt terwijl hij alweer opnieuw wordt aangeroepen. In de IDE gebeurt dat regelmatig; als compiled executable (native code maar met alle checks nog aan) ben ik het nog niet tegengekomen. Blijkbaar heeft de IDE nogal wat extra ballast. :)

Nogmaals, hartstikke bedankt; ik weet dat ik nu op de goede weg zit!

Verwijderd

Topicstarter
Is er dan eigenlijk ook een manier om Redim te omzeilen? Ik had eerst Dim bytBuf(param1) As Byte geprobeerd maar dat slikt de compiler natuurlijk niet omdat ie een niet-dynamische waarden wil hebben om mee te kunnen dimensioneren.

Ik kan niet met zekerheid zeggen dat param1 altijd 32768 is en dus kan ik die waarde niet gebruiken om eenmalig mee te dimensioneren. Doe ik dat wel, dan creëer ik iets van een buffer overflow omdat param1 != ubound(bytBuf) (niet altijd, althans. :) )

  • Tomatoman
  • Registratie: November 2000
  • Laatst online: 28-04 18:15

Tomatoman

Fulltime prutser

Verwijderd schreef op donderdag 29 september 2005 @ 02:20:
param1 neemt helaas echt de waarde 32768 aan en daardoor kan ik z dus niet dimensioneren als Integer maar moet het als Long. Ik had echter begrepen dat Long data types sneller zijn in VB6 dan Integers.
In vrijwel iedere programmeertaal geldt dat het datatype met de beste performance overeenkomt met een datatype dat aansluit op de structuur van de processor. Bij een 32-bits processor(dus iedere pc waar Windows 95/98/NT4/2000/XP op draait), levert een 32-bits integer daarom de beste performance op. Signed of unsigned is daarbij irrelevant.

Wel is het vaak zo dat instructies die integers met een kleinere resolutie (8-bits, 16-bits) even snel worden verwerkt als instructies die 32-bits integers verwerken. Vanuit performance-oogpunt is er daarom meestal geen enkel verschil tussen 8-, 16- en 32-bits integers. Een algemene aanbeveling is dan ook: gebruik native datatypes, wat op moderne processors neerkomt op 32-bits datatypes.

Een goede grap mag vrienden kosten.


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 09-04 22:08
Dat laatste zou ik niet zeggen. Een x86 heeft nog wel oude 8-bits instructies maar veel moderne processoren moeten een 8-bit/16-bit write implementeren als een 32-bit load, mask, OR, en 32 bit write. Niet snel dus.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • farlane
  • Registratie: Maart 2000
  • Laatst online: 29-04 23:38
Verwijderd schreef op donderdag 29 september 2005 @ 02:20:
De code-snelheid lijkt inderdaad beter nu, met minder crashes in de IDE. De reden dat de code zo snel moet zijn is dat het in een functie staat die op een bepaald moment wordt aangeroepen via AddressOf. Als ik het goed begrijp betekent dat dat die functie eigenlijk z'n eigen thread krijgt en multi-threading is sowieso niet fijn in VB6.
AddressOf geeft een functiepointer door. De lib(?) die je aanroept kan vervolgens een callback maken naar jouw functie dmv die pointer. Of dat in een andere thread gebeurt dan je eigen is afhankelijk van de library. ( Als het is Call VB -> LIB [1] --- Call LIB-> VB [2] --> End Call [2]--> End Call [1] dan hoeft het niet een aparte thread te zijn )

Als het wel een aparte thread is raad ik je aan om iets anders te pakken dan VB6 want je gaat het dan niet 100% stabiel krijgen. :)

Somniferous whisperings of scarlet fields. Sleep calling me and in my dreams i wander. My reality is abandoned (I traverse afar). Not a care if I never everwake.


  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
Je zou de redim kunnen "omzeilen" door gewoon een flinke array te nemen waarin het altijd past wat je wenst te doen en dan zelf bijhouden wat het laatste element is. Maar dan ben je natuurlijk niet zo flexibibel en gebruik je waarschijnlijk vaak (veel) meer geheugen dan nodig. Maar dat had je zelf vast ook kunnen bedenken.

Multi-threading is in VB6 best wel te doen, maar inderdaad écht stabiel krijgen zul je het niet. En als het kan zou ik als ik jou was inderdaad de Long's prefereren boven andere data types (behalve dan je byte array)

offtopic:
Overigens zou het ook geen kwaad kunnen als je zelf eens iemand probeert te helpen i.p.v. alleen maar topics te openen en je vragen te dumpen ;) NOFI hoor, maar ik zie je nogal veel vragen en weinig "terug geven" aan de community :>

[ Voor 29% gewijzigd door RobIII op 29-09-2005 13:07 ]

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij


  • .oisyn
  • Registratie: September 2000
  • Laatst online: 13:20

.oisyn

Moderator Devschuur®

Demotivational Speaker

MSalters schreef op donderdag 29 september 2005 @ 10:24:
Dat laatste zou ik niet zeggen. Een x86 heeft nog wel oude 8-bits instructies maar veel moderne processoren moeten een 8-bit/16-bit write implementeren als een 32-bit load, mask, OR, en 32 bit write. Niet snel dus.
maak daar maar 64 bit van geloof ik, maar het haalt niets uit omdat het toch eerst in de cache moet worden geladen (die geloof ik nog eens 32 bytes prefetched ook). Een 32 bit operatie die niet in de cache staat heeft dus eenzelfde stall als een 8 bit operatie. En als het er wel in staat maakt het kwa snelheid al niets meer uit.

rachid.nl: kun je je byte array op een of andere manier accessen als een long array? Dan kun je die not's op elke long doen en doe je het dus 4 bytes per keer. En dan is het handig om ervoor te zorgen dat je de lengte van je bytearray een veelvoud is van 4, zodat je niets 'ingewikkelds' hoeft te doen om die paar losse bytes aan het eind ook nog te converteren (stel je array zou 37 lang zijn, dan kun je met die longs mar 36 bytes converteren en moet je die laatste met de hand doen. Als je de array echter 40 lang maakt, dan kun je die laatste ook met een long doen. Dan doe je weliswaar 3 bytes teveel, maar dat is gratis).

Give a man a game and he'll have fun for a day. Teach a man to make games and he'll never have fun again.


  • MSalters
  • Registratie: Juni 2001
  • Laatst online: 09-04 22:08
.oisyn schreef op donderdag 29 september 2005 @ 13:32:
[...]
maak daar maar 64 bit van geloof ik, maar het haalt niets uit omdat het toch eerst in de cache moet worden geladen (die geloof ik nog eens 32 bytes prefetched ook). Een 32 bit operatie die niet in de cache staat heeft dus eenzelfde stall als een 8 bit operatie. En als het er wel in staat maakt het kwa snelheid al niets meer uit.
64? Ik ken geen ontwerp wat zo radicaal is. De normale bus is wel vaak 64 bits, zelfs op x86, maar de vraag is wat de kleinst mogelijk write is (zonder Read/Modify/Write). In theorie zou dan 1 bit kunnen zijn, x86 is 8, RISC is dacht ik typisch 32. Zelfs dan kunnen string operaties duur worden.

Cache maakt niet zo gek veel uit; main memory gaat inderdaad in veel grotere stukken. Dat doet niets af aan het feit dat je zonder byte writes een read/modify/write moet doen om 8 bits te veranderen. Overigens zorgt cache er wel voor dan een byte array verandering sneller wordt, maar als dat echt kritisch is (en bij de TS geloof ik dat niet heel erg) dan moet je niet met losse bytes prutsen. De operatie is een NOT, die kan inderdaad perfect parallel. Volgens mij kan het zelfs met je MMX unit, en dan heb je dus echt iets veel snellers. Die dingen zijn gebouwd om byte stromen te verwerken.

Man hopes. Genius creates. Ralph Waldo Emerson
Never worry about theory as long as the machinery does what it's supposed to do. R. A. Heinlein


  • RobIII
  • Registratie: December 2001
  • Niet online

RobIII

Admin Devschuur®

^ Romeinse Ⅲ ja!

(overleden)
MSalters schreef op donderdag 29 september 2005 @ 18:13:
Volgens mij kan het zelfs met je MMX unit, en dan heb je dus echt iets veel snellers. Die dingen zijn gebouwd om byte stromen te verwerken.
Bedenk je wel nog even dat we hier met het pre-historische VB6 werken en dat MMX toen nog niet echt gebruikt werd? Laat staan dat je in je compiler iets te vertellen hebt over de instructies die uitgepoept gaan worden en het zou mij eerlijk gezegd verbazen als de hele for-next lus uberhaupt in de L1 cache past nadat het door VB6 gecompiled is :P

There are only two hard problems in distributed systems: 2. Exactly-once delivery 1. Guaranteed order of messages 2. Exactly-once delivery.

Je eigen tweaker.me redirect

Over mij

Pagina: 1